At Meta, our Bug Bounty program is a crucial ingredient of our “defense-in-depth” strategy to safety. Our inner product safety groups examine each bug submission to evaluate its most potential influence in order that we are able to all the time reward exterior researchers primarily based on each the bug they discovered and our additional inner analysis evaluation of the place else the bug may lead us.
Reviews like this assist us proceed to enhance our detection mechanisms in Hermes and throughout our platform. Comparable findings are often awarded between $500 and $3,000. Nonetheless, additional investigation demonstrated how this vulnerability may have been was arbitrary code execution. This resulted in a $12,000 complete bounty payout.
To make issues extra enjoyable, and to display the influence of what we discovered, we programmed our exploit to run the traditional online game Doom (1993) straight from inside Hermes.
Hermes is an open supply JS engine optimized for React Native. It options ahead-of-time compilation to scale back startup time and reminiscence utilization, making it significantly appropriate for cell purposes. At Meta, for instance, Hermes is used to run the JS code for results in Spark AR, our augmented actuality improvement platform. In July 2020, we launched the Hermes/Spark AR monitor to additional incentivize safety analysis on Hermes.
The bug, sorting the unsortable
In August, 2020, we obtained a report displaying a crash in Hermes attributable to an OOB learn. The report featured a tangled JS file of about 180 strains of code, most probably created by a fuzzing engine. After some cleanup, we pinpointed the foundation trigger.
The crash occurred whereas executing the Array.prototype.kind technique, in code associated to some latest modifications to make Hermes’s kind implementation secure. A sorting algorithm is claimed to be secure if the order of equal parts stays the identical after sorting.
In Hermes, this was achieved through the use of a separate index vector prefilled with the ingredient’s authentic indices  (see code feedback). Each time two parts within the array are swapped, their indices are swapped as nicely . If two array parts are equal, their authentic indices’ values are in contrast as a substitute . This ensures stability.
ExecutionStatus quickSort(SortModel *sm, uint32_t start, uint32_t finish) uint32_t len = finish - start; //  std::vector
index(len); // Array of authentic indices of things for (uint32_t i = 0; i < len; ++i) index[i] = i; ... return doQuickSort(sm, index, /* ... */ ); ... ExecutionStatus _swap(SortModel *sm, std::vector &index, uint32_t i, uint32_t j) if (sm->swap(i, j) == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; //  std::swap(index[i], index[j]); ... ... CallResult _less(SortModel *sm, std::vector &index, uint32_t i, uint32_t j) auto res = sm->examine(i, j); if (res == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; //  return (*res != 0) ? (*res < 0) : (index[i] < index[j]);
Array.prototype.kind can take, as an argument, a operate defining a customized kind order:
const array = [3,1,4,2]; array.kind(operate compareFn(first, second) ... );
However what occurs if we use this callback to switch the array whereas sorting? When kind is first known as, it can use the preliminary model of the array, however after executing the comparability operate, kind will see the modified model.
Alternatively, index , whose size displays the preliminary array’s dimension, stays unchanged. Because of this, the array and index size would possibly differ. This would possibly occur, for instance, if new parts are added to the array from throughout the callback.
On this case, accessing parts past the unique array’s dimension throughout sorting will end in OOB accesses in index.
Understanding the Quicksort algorithm
To know how you can management which parts get accessed, we have to perceive the underlying Quicksort algorithm.
Quicksort begins by choosing a pivot ingredient, picked to be the median of three parts. It then searches for a component bigger than pivot (as reported by the callback operate) from left to proper (i). As soon as this bigger ingredient is discovered, the algorithm appears for a component smaller than pivot from proper to left (j). These parts are then swapped and the process is repeated till the 2 searches “cross each other,” at which level the pivot is swapped with the smaller ingredient (j).
Inflicting an OOB learn
Now, let’s put all the things collectively and use the callback to trick the sorting algorithm implementation into sorting parts past the unique array:
- We begin by extending the array past its authentic dimension (so to be bigger than index).
- We then wait to get previous the median-of-three step (used to pick out a pivot), which requires three comparisons.
- At this level, we preserve returning -1 to advance the left index i till we've got reached the goal cell previous the unique array’s dimension.
- Lastly, we return 0, indicating that the 2 parts are equal. This may trigger an OOB learn to the index buffer.
var array = [1,2,3,4,5,6,7,8,9,10]; const initial_length = array.size const extended_length = initial_length*2 var cnt = 0 array.kind(operate compareFn(first, second) cnt++; //  Prolong the array if (cnt == 1) for (i=0; i
3) // i represents the left index shifting to the suitable const i = cnt - 1; // target_cell may be something between initial_length and extended_length const target_cell = 15; if (i != target_cell) //  If we did not but attain the goal cell, preserve advancing return -1; else //  If we did attain the goal cell, set off an OOB index lookup print("Search for the crash...") return 0; );
Executing the above code on an ASAN construct of Hermes produced the next crash:
Search for the crash... ================================================================= ==1519183==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of dimension 4 at 0x604000003fc8 thread T0 SCARINESS: 27 (4-byte-read-heap-buffer-overflow-far-from-bounds) #0 0xcd011a in hermes::vm::_less(...) hermes/lib/VM/JSLib/Sorting.cpp:35 #1 0xccf3de in hermes::vm::quickSortPartition(...) hermes/lib/VM/JSLib/Sorting.cpp:175 #2 0xccf3de in hermes::vm::doQuickSort(...) hermes/lib/VM/JSLib/Sorting.cpp:262 #3 0xccef6e in hermes::vm::quickSort(...) hermes/lib/VM/JSLib/Sorting.cpp:332 #4 0x953f41 in hermes::vm::arrayPrototypeSort(...) hermes/lib/VM/JSLib/Array.cpp:1248 #5 0x8b3ee9 in hermes::vm::NativeFunction::_nativeCall(...) hermes/embody/hermes/VM/Callable.h:539 #6 0xa27a2f in hermes::vm::Interpreter::handleCallSlowPath(...) hermes/lib/VM/Interpreter.cpp:318 ...
Let the hacking start
After understanding the foundation reason behind a bug, we instantly work with the related product/function improvement workforce to difficulty a repair. Within the meantime, we all the time ask ourselves, “How serious is this?” and work to find out the bug’s most potential influence.
So, how critical was this Hermes bug? We have now seen the way it may very well be used to entry parts past index, however may it have been used to acquire one thing greater than a crash?
Swapping the unswappable
Quicksort works by swapping parts and, when two parts are swapped, their respective indices are additionally swapped. Subsequently, not less than in concept, this bug may be used to swap indices with neighboring parts outdoors the index vector.
In that case, we may receive the power to do some loosely managed writes out of the index.
There are two kinds of swaps in Quicksort:
- Between i and j
- Between j and the pivot
We determined to have a look at the primary choice for the reason that sorting algorithm lets us management each i and j:
whereas (true) //  whereas (true) ++i; res = _less(sm, index, i, pivot); if (res == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; if (!*res) break; //  whereas (true) --j; res = _less(sm, index, pivot, j); if (res == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; if (!*res) break; //  if (i >= j) break; //  if (_swap(sm, index, i, j) == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION;
The swap  occurs as soon as i is pointing to a component smaller than pivot  and j is pointing to a component bigger than pivot .
An intermediate verify  ensures that i and j are swapped provided that they haven’t crossed one another (i.e., i < j ), however to be able to swap parts out of the index, i and j should cross.
To unravel that, we determined to underflow j (a 32-bit unsigned integer) in order that j = 0xffffffff.
In a 32-bit system, index[0xffffffff] is equal to index[-1], which corresponds to the ingredient instantly earlier than the index.
Sadly, studying j = 0xffffffff will trigger the applying to hold within the loop at . It's because array[0xffffffff] is “empty,” which, in accordance with the specs, is taken into account to be larger than all the things. Because of this, the loop trying to find a component smaller than the pivot will preserve looping till the entire 32-bit handle house has been explored.
To unravel for this we outlined a customized getter in order that array[0xffffffff]won't return an empty ingredient.
At this level, we have been capable of swap index[j] and index[i], the place j = 0xffffffff. Discover that we are able to additionally arbitrarily decrement j and/or increment i, so long as i < j.
Management what you learn, management what you write
We have now restricted management over the content material of the index, so we determined to deal with swapping content material in close by reminiscence.
The index is an occasion of std::vector, and its underlying reminiscence is allotted through malloc. If we may in some way create a reminiscence structure the place we management the content material earlier than or after the index, then we might be capable of management what we write throughout a swap.
A straightforward option to management the reminiscence structure of malloc allocations is to make use of mmap’d allocations. For big sufficient allocations, malloc will use mmap to require extra reminiscence from the system. When mapping new reminiscence through mmap, the Linux kernel will attempt to broaden recognized mapped reminiscence and return adjoining areas. We will exploit this logic to position particular person allocations alongside.
Most knowledge buildings in Hermes don’t use malloc straight however as a substitute depend on the Hermes Rubbish Collector (GC) to allocate reminiscence. Nonetheless, Hermes’s implementation of ArrayBuffer makes use of malloc to allocate their underlying knowledge buffer.
For our exploit, we crafted this strategic reminiscence structure:
To acquire it, we first created two ArrayBuffers (C and B), began sorting (thus creating the index), and eventually created a 3rd ArrayBuffer (A) from throughout the callback. Right here, A, B, and C are the info buffers of ArrayBuffers that we management.
Utilizing our swapping capabilities, we are able to now swap parts of A with parts of B or C.
Acquiring arbitrary reads and writes
Reminiscence chunks allotted through malloc embed further metadata, which, as an example, comprises the dimensions of the allotted area.
All our reminiscence areas (A, Index, B, and C) are contained in separate malloc chunks, every having its personal header and metadata.
What would occur if we swapped the dimensions of a malloc chunk with one other worth?
Swapping the dimensions of a piece with one other worth results in some attention-grabbing outcomes. For instance, if we write a dimension giant sufficient to include each B and C and delete B’s ArrayBuffer occasion (thus inflicting its reminiscence to be freed), then the reminiscence of each B and C can be unmapped. Nonetheless, since we by no means deleted C’s ArrayBuffer occasion, it could nonetheless be pointing to the reminiscence (which is now invalid).
However look what occurs if we allocate a brand new ArrayBuffer, B’, with a dimension matching the newly unmapped area:
The system will reallocate the unmapped area for B’, and, consequently, B’ and C will overlap. Acquiring overlapping chunks is a well known heap exploitation approach and may be typically used to acquire extra highly effective capabilities, comparable to arbitrary reads or writes.
On this setting, we are able to management the content material of C from B’. To acquire learn/write capabilities, we are able to repurpose area C to host “something” containing reminiscence addresses, in order that from B’ we may change these to any arbitrary handle.
There's a approach we are able to get inner knowledge buildings for JS objects allotted inside C. Extra particularly, we are able to do that through the use of the Hermes GC. The GC is liable for releasing or allocating reminiscence for Hermes objects. On the time of the report, the default GC was GenGC, and, as outlined within the GenGC documentation, the house allotted by this GC:
“… is made out of many fixed-size regions of memory known as segments. Currently these segments are 4 MiB … Memory is acquired and released on a per-segment basis. Segments are allocated using mmap …”
If we free C and create sufficient JS objects, the GC might want to allocate an additional phase (to host the brand new objects). The brand new phase can be positioned in C’s place (assuming C is not less than 4 MiB in dimension).
In essence, we're taking the reminiscence beforehand used to carry the content material of ArrayBuffer C and get Hermes to reuse it for a JS heap phase (bear in mind the dotted part of our earlier diagram?). Whereas doing so, we're sustaining learn/write (RW) entry to the phase through B’.
Now we are able to overwrite the content material of the phase S from B’.
GC segments include loads of JS object situations, together with ArrayBuffer objects. If we spray sufficient ArrayBuffers, a few of them can be positioned within the new phase:
To summarize, we obtained Hermes to reuse C’s reminiscence to host the “metadata” of a number of different ArrayBuffers. This was doable as a result of ArrayBuffers’ contents and JS object metadata are each in the end backed by mmap (see our earlier diagram).
Every ArrayBuffer object comprises a pointer to the underlying knowledge buffer (i.e., its content material).
Utilizing B’, we are able to change the handle in a selected occasion and make it level anyplace in reminiscence as a substitute of to their authentic buffer (D within the picture). Then, by studying or writing to that ArrayBuffer occasion, we are able to learn or write at any arbitrary handle!
Placing all of it collectively
We got here a good distance from a mysterious crash in Array.prototype.kind to acquiring limitless R/W capabilities.
- We created a strategic reminiscence structure of 4 adjoining mmap’d buffers.
- We used the sorting bug to swap the dimensions of a malloc chunk and procure two overlapping ArrayBuffers.
- Lastly, we changed one of many overlapping buffers with a GC phase in order that we may overwrite pointers and procure arbitrary R/W entry.
A typical exploit stream would proceed as follows:
- Write an ROP-chain and shellcode someplace in reminiscence.
- Leak some addresses to defeat ASLR.
- Overwrite operate pointer with a stack pivoting gadget.
- Execute ROP-chain and make shellcode executable.
- Bounce to shellcode.
We won't cowl the main points of the remaining steps, as they consult with well-known exploitation strategies. The ultimate outcome lets us execute arbitrary code straight from Hermes and escape the JS sandbox.
To make issues extra enjoyable, we used our exploit to run Doom:
Because of our bug bounty researcher’s fast discover and report (inside six days of the bug being launched!) and the workforce’s work on it, this bug wasn’t included in any Hermes public releases.
Primarily based on our inner investigation, we awarded the researcher with a $12,000 bounty. Whereas we all the time carry out an in-depth investigation of every bug submission, we strongly encourage researchers to dive deeper in our native merchandise. On that notice, we award bonuses to reviews that function a full proof of idea.
This and different reviews have been submitted throughout the context of our Hermes and SparkAR bug bounty monitor, which incentivizes safety analysis on these merchandise.
Since this bug was reported, we've got improved our inner fuzzing effort to assist discover bugs earlier on, and we're at present engaged on hardening measures that will drastically diminish the influence of comparable bugs.
We encourage researchers to share their findings and work with our inner analysis workforce to check our mitigations so we are able to preserve bettering our defenses. We’ll proceed to share notable bugs and bug bounty reviews to rejoice the work of our group. Comfortable searching!