{"id":609,"date":"2022-07-20T16:01:56","date_gmt":"2022-07-20T16:01:56","guid":{"rendered":"https:\/\/fde.cat\/index.php\/2022\/07\/20\/using-hermess-quicksort-to-run-doom-a-tale-of-javascript-exploitation\/"},"modified":"2022-07-20T16:01:56","modified_gmt":"2022-07-20T16:01:56","slug":"using-hermess-quicksort-to-run-doom-a-tale-of-javascript-exploitation","status":"publish","type":"post","link":"https:\/\/fde.cat\/index.php\/2022\/07\/20\/using-hermess-quicksort-to-run-doom-a-tale-of-javascript-exploitation\/","title":{"rendered":"Using Hermes\u2019s Quicksort to run Doom: A tale of JavaScript exploitation"},"content":{"rendered":"<p><span>At Meta, our <a href=\"https:\/\/www.facebook.com\/whitehat\" target=\"_blank\" rel=\"noopener\">Bug Bounty program<\/a> is an important element of our \u201c<\/span><a href=\"https:\/\/about.fb.com\/news\/2019\/01\/designing-security-for-billions\/\" target=\"_blank\" rel=\"noopener\"><span>defense-in-depth<\/span><\/a><span>\u201d approach to security. Our internal product security teams investigate every bug submission to assess its maximum potential impact so that we can always reward external researchers based on both the bug they found and our further internal research assessment of where else the bug could lead us.<\/span><\/p>\n<p><span>We want to share more about how this process works. To do this, we\u2019re sharing details of an investigation into a bug report we received in August 2020 concerning <\/span><a href=\"https:\/\/engineering.fb.com\/2019\/07\/12\/android\/hermes\/\" target=\"_blank\" rel=\"noopener\"><span>Hermes<\/span><\/a><span>, an open source JavaScript (JS) engine developed by Meta. This initial report showed a peculiar bug in Hermes\u2019s<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Quicksort\" target=\"_blank\" rel=\"noopener\"> <span>Quicksort<\/span><\/a><span> implementation, resulting in blind out-of-bounds (OOB) memory reads.<\/span><\/p>\n<p><span>Reports like this help us continue to improve our detection mechanisms in Hermes and across our platform. Similar findings are usually awarded between $500 and $3,000. However, further investigation demonstrated how this vulnerability could have been turned into arbitrary code execution. This resulted in a $12,000 total bounty payout.<\/span><\/p>\n<p><span>To make things more fun, and to demonstrate the impact of what we found, we programmed our exploit to run the classic video game <\/span><span>Doom<\/span><span> (1993) directly from within Hermes.<\/span><\/p>\n<h2><span>Hermes, a lightweight JavaScript engine<\/span><\/h2>\n<p><a href=\"https:\/\/hermesengine.dev\/\" target=\"_blank\" rel=\"noopener\"><span>Hermes<\/span><\/a><span> is an open source JS engine optimized for<\/span><a href=\"https:\/\/reactnative.dev\/blog\/2021\/10\/26\/toward-hermes-being-the-default\" target=\"_blank\" rel=\"noopener\"> <span>React Native<\/span><\/a><span>. It features<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Ahead-of-time_compilation\" target=\"_blank\" rel=\"noopener\"> <span>ahead-of-time compilation<\/span><\/a><span> to reduce startup time and memory usage, making it particularly suitable for mobile applications. At Meta, for example, Hermes is used to run the JS code for effects in<\/span><a href=\"https:\/\/sparkar.facebook.com\/ar-studio\/\" target=\"_blank\" rel=\"noopener\"> <span>Spark AR<\/span><\/a><span>, our augmented reality development platform. In July 2020, we launched the <\/span><a href=\"https:\/\/www.facebook.com\/notes\/685647469051301\" target=\"_blank\" rel=\"noopener\"><span>Hermes\/Spark AR <\/span><\/a><span>track to further incentivize security research on Hermes.<\/span><\/p>\n<h2>The bug, sorting the unsortable<\/h2>\n<p><span>In August, 2020, we received a report showing a crash in Hermes caused by an<\/span><a href=\"https:\/\/cwe.mitre.org\/data\/definitions\/125.html\" target=\"_blank\" rel=\"noopener\"> <span>OOB read<\/span><\/a><span>. The report featured a tangled JS file of about 180 lines of code, most likely created by a<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Fuzzing\" target=\"_blank\" rel=\"noopener\"> <span>fuzzing<\/span><\/a><span> engine. After some cleanup, we pinpointed the root cause.<\/span><\/p>\n<p><span>The crash occurred while executing the<\/span><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Array\/sort\" target=\"_blank\" rel=\"noopener\"> <span>Array.prototype.sort method<\/span><\/a><span>, in code related to some recent<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/commit\/8a5d054a8681e31e2d479522c0f91e05bb8a38f7\" target=\"_blank\" rel=\"noopener\"> <span>changes<\/span><\/a><span> to make Hermes\u2019s <\/span><span>sort<\/span><span> implementation<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Sorting_algorithm#Stability\"> <span>stable<\/span><\/a><span>. A sorting algorithm is said to be stable if the order of equal elements remains the same after sorting.<\/span><\/p>\n\n<p><span>In Hermes, this was achieved by using a separate <\/span><span>index<\/span><span> vector<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/8a5d054a8681e31e2d479522c0f91e05bb8a38f7\/lib\/VM\/JSLib\/Sorting.cpp#L324-L327\" target=\"_blank\" rel=\"noopener\"> <span>prefilled<\/span><\/a><span> with the element\u2019s original indices [1] (see code comments). Whenever two elements in the array are swapped, their indices are<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/8a5d054a8681e31e2d479522c0f91e05bb8a38f7\/lib\/VM\/JSLib\/Sorting.cpp#L44\" target=\"_blank\" rel=\"noopener\"> <span>swapped as well<\/span><\/a><span> [2]. If two array elements are equal, their original indices\u2019 values<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/8a5d054a8681e31e2d479522c0f91e05bb8a38f7\/lib\/VM\/JSLib\/Sorting.cpp#L35\" target=\"_blank\" rel=\"noopener\"> <span>are compared<\/span><\/a><span> instead [3]. This guarantees stability.<\/span><\/p>\n<p>ExecutionStatus quickSort(SortModel *sm, uint32_t begin, uint32_t end) {<br \/>\n  uint32_t len = end &#8211; begin;<br \/>\n  \/\/ [1]<br \/>\n  std::vector&lt;uint32_t&gt; index(len); \/\/ Array of original indices of items<br \/>\n  for (uint32_t i = 0; i &lt; len; ++i) {<br \/>\n    index[i] = i;<br \/>\n  }<br \/>\n  &#8230;<br \/>\n  return doQuickSort(sm, index, \/* &#8230; *\/ );<br \/>\n}<\/p>\n<p>&#8230;<\/p>\n<p>ExecutionStatus<br \/>\n_swap(SortModel *sm, std::vector&lt;uint32_t&gt; &amp;index, uint32_t i, uint32_t j) {<br \/>\n  if (sm-&gt;swap(i, j) == ExecutionStatus::EXCEPTION) {<br \/>\n    return ExecutionStatus::EXCEPTION;<br \/>\n  }<br \/>\n  \/\/ [2]<br \/>\n  std::swap(index[i], index[j]);<br \/>\n  &#8230;<br \/>\n}<\/p>\n<p>&#8230;<\/p>\n<p>CallResult&lt;bool&gt;<br \/>\n_less(SortModel *sm, std::vector&lt;uint32_t&gt; &amp;index, uint32_t i, uint32_t j) {<br \/>\n  auto res = sm-&gt;compare(i, j);<br \/>\n  if (res == ExecutionStatus::EXCEPTION) {<br \/>\n    return ExecutionStatus::EXCEPTION;<br \/>\n  }<br \/>\n  \/\/ [3]<br \/>\n  return (*res != 0) ? (*res &lt; 0) : (index[i] &lt; index[j]);<br \/>\n}<\/p>\n<p><span>Array.prototype.sort can take, as an argument, a function defining a custom sort order:<\/span><\/p>\n<p>const array = [3,1,4,2];<br \/>\narray.sort(function compareFn(first, second) { &#8230; });<\/p>\n<p><span>But what happens if we use this callback to modify the array while sorting? When <span>sort<\/span> <\/span><span>is first called, it will use the initial version of the array, but after executing the comparison function, <span>sort<\/span> <\/span><span>will see the modified version.<\/span><\/p>\n<p><span>On the other hand, <span>index<\/span> <\/span><span>, whose length reflects the initial array\u2019s size, remains unchanged. As a result, the array and index length might differ. This might happen, for example, if new elements are added to the array from within the callback.<\/span><\/p>\n<p class=\"line-numbers\"><span>In this case, accessing elements beyond the original array\u2019s size during sorting will result in OOB accesses in <span>index.<\/span><\/span><\/p>\n<h3><span>Understanding the Quicksort algorithm<\/span><\/h3>\n<p><span>To understand how to control which elements get accessed, we need to understand the underlying<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Quicksort\" target=\"_blank\" rel=\"noopener\"> <span>Quicksort algorithm<\/span><\/a><span>.<\/span><\/p>\n<p><span>Quicksort starts by selecting a pivot element, picked to be the median of three elements. It then searches for an element larger than pivot (as reported by the callback function) from left to right (<\/span><span>i<\/span><span>). Once this larger element is found, the algorithm looks for an element smaller than pivot from right to left (<\/span><span>j<\/span><span>). Those elements are then swapped and the procedure is repeated until the two searches \u201ccross each other,\u201d at which point the pivot is swapped with the smaller element (<\/span><span>j<\/span><span>).<\/span><\/p>\n\n<h3><span>Causing an OOB read<\/span><\/h3>\n<p><span>Now, let\u2019s put everything together and use the callback to trick the sorting algorithm implementation into sorting elements beyond the original array:<\/span><\/p>\n<p><span>We start by extending the array beyond its original size (so to be larger than <span>index<\/span><\/span><span>).<\/span><br \/>\n<span>We then wait to get past the <\/span><span>median-of-three<\/span><span> step (used to select a pivot), which requires three comparisons.<\/span><br \/>\n<span>At this point, we keep returning -1 to advance the left index <\/span><span>i<\/span><span> until we have reached the target cell past the original array\u2019s size.<\/span><br \/>\n<span>Finally, we return 0, indicating that the two elements are equal. This will cause an OOB read to the index buffer.<\/span><\/p>\n<p>var array = [1,2,3,4,5,6,7,8,9,10];<br \/>\nconst initial_length = array.length<br \/>\nconst extended_length = initial_length*2<br \/>\nvar cnt = 0<\/p>\n<p>array.sort(function compareFn(first, second) {<\/p>\n<p>  cnt++;<\/p>\n<p>  \/\/ [1] Extend the array<br \/>\n  if (cnt == 1){<br \/>\n    for (i=0; i&lt;extended_length-initial_length; i++){<br \/>\n      array.push(&#8216;X&#8217;)<br \/>\n    }<br \/>\n  }<\/p>\n<p>  \/\/ [2] Get past the median-of-three step<br \/>\n  if (cnt &gt; 3){<\/p>\n<p>    \/\/ i represents the left index moving to the right<br \/>\n    const i = cnt &#8211; 1;<\/p>\n<p>    \/\/ target_cell can be anything between initial_length and extended_length<br \/>\n    const target_cell = 15;<\/p>\n<p>    if (i != target_cell){<br \/>\n      \/\/ [3] If we didn&#8217;t yet reach the target cell, keep advancing<br \/>\n      return -1;<br \/>\n    } else {<br \/>\n      \/\/ [4] If we did reach the target cell, trigger an OOB index lookup<br \/>\n      print(&#8220;Look for the crash&#8230;&#8221;)<br \/>\n      return 0;<br \/>\n    }<br \/>\n  }<br \/>\n});<\/p>\n<p><span>Executing the above code on an<\/span><a href=\"https:\/\/github.com\/google\/sanitizers\/wiki\/AddressSanitizer\" target=\"_blank\" rel=\"noopener\"> <span>ASAN<\/span><\/a><span> build of Hermes produced the following crash:<\/span><\/p>\n<p>Look for the crash&#8230;<br \/>\n=================================================================<br \/>\n==1519183==ERROR: AddressSanitizer: heap-buffer-overflow &#8230;<br \/>\nREAD of size 4 at 0x604000003fc8 thread T0<br \/>\nSCARINESS: 27 (4-byte-read-heap-buffer-overflow-far-from-bounds)<br \/>\n#0 0xcd011a in hermes::vm::_less(&#8230;) hermes\/lib\/VM\/JSLib\/Sorting.cpp:35<br \/>\n#1 0xccf3de in hermes::vm::quickSortPartition(&#8230;) hermes\/lib\/VM\/JSLib\/Sorting.cpp:175<br \/>\n#2 0xccf3de in hermes::vm::doQuickSort(&#8230;) hermes\/lib\/VM\/JSLib\/Sorting.cpp:262<br \/>\n#3 0xccef6e in hermes::vm::quickSort(&#8230;) hermes\/lib\/VM\/JSLib\/Sorting.cpp:332<br \/>\n#4 0x953f41 in hermes::vm::arrayPrototypeSort(&#8230;) hermes\/lib\/VM\/JSLib\/Array.cpp:1248<br \/>\n#5 0x8b3ee9 in hermes::vm::NativeFunction::_nativeCall(&#8230;) hermes\/include\/hermes\/VM\/Callable.h:539<br \/>\n#6 0xa27a2f in hermes::vm::Interpreter::handleCallSlowPath(&#8230;) hermes\/lib\/VM\/Interpreter.cpp:318<br \/>\n&#8230;<\/p>\n<h2><span>Let the hacking begin<\/span><\/h2>\n<p><span>After understanding the root cause of a bug, we immediately work with the relevant product\/feature development team to issue a fix. In the meantime, we always ask ourselves, \u201cHow serious is this?\u201d and work to determine the bug\u2019s maximum potential impact.<\/span><\/p>\n<p><span>So, how serious was this Hermes bug? We have seen how it could be used to access elements beyond <\/span><span>index<\/span><span>, but could it have been used to obtain something more than a crash?<\/span><\/p>\n<h3><span>Swapping the unswappable<\/span><\/h3>\n<p><span>Quicksort works by swapping elements and, when two elements are swapped, their respective indices are also swapped. Therefore, at least in theory, this bug might be used to swap indices with neighboring elements outside the index vector.<\/span><\/p>\n<p><span>If so, we could obtain the ability to do some loosely controlled writes out of the index.<\/span><\/p>\n<p><span>There are two types of swaps in Quicksort:<\/span><\/p>\n<p><span>Between <\/span><span>i<\/span><span> and <\/span><span>j<\/span><br \/>\n<span>Between <\/span><span>j<\/span><span> and the pivot<\/span><\/p>\n<p><span>We decided to look at the first option since the sorting algorithm lets us control both <\/span><span>i <\/span><span>and <\/span><span>j<\/span><span>:<\/span><\/p>\n<p>while (true) {<br \/>\n    \/\/ [1]<br \/>\n    while (true) {<br \/>\n      ++i;<br \/>\n      res = _less(sm, index, i, pivot);<br \/>\n      if (res == ExecutionStatus::EXCEPTION) {<br \/>\n        return ExecutionStatus::EXCEPTION;<br \/>\n      }<br \/>\n      if (!*res) {<br \/>\n        break;<br \/>\n      }<br \/>\n    }<br \/>\n    \/\/ [2]<br \/>\n    while (true) {<br \/>\n      &#8211;j;<br \/>\n      res = _less(sm, index, pivot, j);<br \/>\n      if (res == ExecutionStatus::EXCEPTION) {<br \/>\n        return ExecutionStatus::EXCEPTION;<br \/>\n      }<br \/>\n      if (!*res) {<br \/>\n        break;<br \/>\n      }<br \/>\n    }<br \/>\n    \/\/ [3]<br \/>\n    if (i &gt;= j) {<br \/>\n      break;<br \/>\n    }<\/p>\n<p>    \/\/ [4]<br \/>\n    if (_swap(sm, index, i, j) == ExecutionStatus::EXCEPTION) {<br \/>\n      return ExecutionStatus::EXCEPTION;<br \/>\n    }<br \/>\n  }<\/p>\n<p><span>The swap [4] happens once <\/span><span>i<\/span><span> is pointing to an element smaller than pivot [1] and <\/span><span>j<\/span><span> is pointing to an element larger than pivot [2].<\/span><\/p>\n<p class=\"line-numbers\"><span>An intermediate check [3] ensures that <\/span><span>i<\/span><span> and <\/span><span>j<\/span><span> are swapped only if they haven\u2019t crossed each other (<\/span><span>i.e.<\/span><span>, <\/span><span>i &lt; j<\/span><span> ), but in order to swap elements out of the index, <\/span><span>i<\/span><span> and <\/span><span>j<\/span><span> must cross.<\/span><\/p>\n<p><span>To solve that, we decided to<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Integer_overflow\" target=\"_blank\" rel=\"noopener\"> <span>underflow<\/span><\/a> <span>j <\/span><span>(a 32-bit unsigned integer) so that <\/span><span>j = 0xffffffff<\/span><span>.<\/span><\/p>\n<p><span>In a 32-bit system, <\/span><span>index[0xffffffff]<\/span><span> is equivalent to <\/span><span>index[-1]<\/span><span>, which corresponds to the element immediately before the index.<\/span><\/p>\n<p><span>Unfortunately, reading <span>j = 0xffffffff <\/span><\/span><span>will cause the application to hang in the loop at [2]. This is because <\/span><span>array[0xffffffff]<\/span><span> is \u201cempty,\u201d which, according to the<\/span><a href=\"https:\/\/tc39.es\/ecma262\/multipage\/indexed-collections.html#sec-array.prototype.sort\" target=\"_blank\" rel=\"noopener\"> <span>specs<\/span><\/a><span>, is considered to be greater than everything. As a result, the loop searching for an element smaller than the pivot will keep looping until the whole 32-bit address space has been explored.<\/span><\/p>\n<p><span>To solve for this we defined a custom getter so that <span>array[0xffffffff]<\/span><\/span><span>will not return an empty element.<\/span><\/p>\n<p class=\"line-numbers\">Array.prototype.__defineGetter__(0xffffffff, &#8230;);<\/p>\n<p><span>At this point, we were able to swap <span>index[j]<\/span><\/span><span> and <span>index[i]<\/span><\/span><span>, where <span>j = 0xffffffff<\/span><\/span><span>. Notice that we can also arbitrarily decrement <\/span><span>j<\/span><span> and\/or increment <\/span><span>i<\/span><span>,<\/span> <span>as long as <\/span><span>i &lt; j<\/span><span>.<\/span><\/p>\n\n<h3><span>Control what you read, control what you write<\/span><\/h3>\n<p><span>We have limited control over the content of the index, so we decided to focus on swapping content in nearby memory.<\/span><\/p>\n<p><span>The index is an instance of<\/span><a href=\"https:\/\/www.cplusplus.com\/reference\/vector\/vector\/\" target=\"_blank\" rel=\"noopener\"> <span>std::vector<\/span><\/a><span>, and its underlying memory is allocated via<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/C_dynamic_memory_allocation\" target=\"_blank\" rel=\"noopener\"> <span>malloc<\/span><\/a><span>. If we could somehow create a memory layout where we control the content before or after the index, then we would be able to control what we write during a swap.<\/span><\/p>\n<p><span>An easy way to control the memory layout of malloc allocations is to use <\/span><span>mmap\u2019d allocation<\/span><span>s. For large enough allocations, malloc will<\/span><a href=\"https:\/\/github.com\/lattera\/glibc\/blob\/master\/malloc\/malloc.c#L2282-L2287\" target=\"_blank\" rel=\"noopener\"> <span>use mmap<\/span><\/a><span> to require more memory from the system. When mapping new memory via<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Mmap\" target=\"_blank\" rel=\"noopener\"> <span>mmap<\/span><\/a><span>, the Linux kernel will<\/span><a href=\"https:\/\/github.com\/torvalds\/linux\/blob\/83c1fd763b3220458652f7d656d5047292ebcde4\/mm\/mmap.c#L1715\" target=\"_blank\" rel=\"noopener\"><span> try to expand<\/span><\/a><span> known mapped memory and return adjacent regions. We can exploit this logic to place individual allocations alongside.<\/span><\/p>\n<p><span>Most data structures in Hermes don\u2019t use malloc directly but instead rely on the Hermes<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Garbage_collection_(computer_science)\" target=\"_blank\" rel=\"noopener\"> <span>Garbage Collector (GC)<\/span><\/a><span> to allocate memory. However, Hermes\u2019s implementation of<\/span><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/ArrayBuffer\" target=\"_blank\" rel=\"noopener\"> <span>ArrayBuffer<\/span><\/a><span> uses<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/6c4ac7e3b4120a659110e6da69a68399bcd54b57\/lib\/VM\/JSArrayBuffer.cpp#L201-L204\" target=\"_blank\" rel=\"noopener\"> <span>malloc<\/span><\/a><span> to<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/6c4ac7e3b4120a659110e6da69a68399bcd54b57\/lib\/VM\/JSArrayBuffer.cpp#L201-L204\" target=\"_blank\" rel=\"noopener\"> <span>allocate<\/span><\/a><span> their underlying data buffer.<\/span><\/p>\n\n<p><span>For our exploit, we crafted this strategic memory layout:<\/span><\/p>\n\n<p><span>To obtain it, we first created two ArrayBuffers (C and B), started sorting (thus creating the index), and finally created a third ArrayBuffer (A) from within the callback. Here, A, B, and C are the data buffers of ArrayBuffers that we control.<\/span><\/p>\n<p><span>Using our swapping capabilities, we can now swap elements of A with elements of B or C.<\/span><\/p>\n<h3><span>Obtaining arbitrary reads and writes<\/span><\/h3>\n<p><span>Memory chunks allocated via malloc<\/span><a href=\"https:\/\/sourceware.org\/glibc\/wiki\/MallocInternals\" target=\"_blank\" rel=\"noopener\"> <span>embed extra metadata<\/span><\/a><span>, which, for instance, contains the size of the allocated region.<\/span><\/p>\n<p><span>All our memory regions (A, Index, B, and C) are contained in separate malloc chunks, each having its own header and metadata.<\/span><\/p>\n<p><span>What would happen if we swapped the size of a malloc chunk with another value?<\/span><\/p>\n\n<p><span>Swapping the size of a chunk with another value leads to some interesting results. For example, if we write a size large enough to contain both B and C and delete B\u2019s ArrayBuffer instance (thus causing its memory to be freed), then the memory of both B and C will be unmapped. However, since we never deleted C\u2019s ArrayBuffer instance, it would still be pointing to the memory (which is now invalid).<\/span><\/p>\n\n<p><span>But look what happens if we allocate a new ArrayBuffer, B\u2019, with a size matching the newly unmapped region:\u00a0<\/span><\/p>\n\n<p><span>The system will reallocate the unmapped region for B\u2019, and, as a result, B\u2019 and C will overlap.<\/span><a href=\"https:\/\/github.com\/shellphish\/how2heap\/blob\/master\/glibc_2.31\/mmap_overlapping_chunks.c\"> <span>Obtaining overlapping chunks<\/span><\/a><span> is a well-known heap exploitation technique and can be often used to obtain more powerful capabilities, such as arbitrary reads or writes.<\/span><\/p>\n<p><span>In this setting, we can control the content of C from B\u2019. To obtain read\/write capabilities, we can repurpose region C to host \u201csomething\u201d containing memory addresses, so that from B\u2019 we could change these to any arbitrary address.<\/span><\/p>\n<p><span>There is a way we can get <\/span>internal data structures for JS objects allocated inside C<span>. More specifically, we can do this by using the Hermes GC. The GC is responsible for releasing or allocating memory for Hermes objects. At the time of the report, the default GC was<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/c935791373e5767605e87196ec9eb8ee0c3e1e65\/doc\/GenGC.md\" target=\"_blank\" rel=\"noopener\"> <span>GenGC<\/span><\/a><span>, and, as outlined in the <\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/blob\/c935791373e5767605e87196ec9eb8ee0c3e1e65\/doc\/GenGC.md\" target=\"_blank\" rel=\"noopener\"><span>GenGC documentation<\/span><\/a><span>, the space allocated by this GC:<\/span><\/p>\n<p><span>\u201c\u2026 is made out of many fixed-size regions of memory known as segments. Currently these segments are 4 MiB \u2026 Memory is acquired and released on a per-segment basis. Segments are allocated using mmap \u2026\u201d<\/span><\/p>\n<p><span>If we free C and create enough JS objects, the GC will need to allocate an extra segment (to host the new objects). The new segment will be placed in C\u2019s place (assuming C is at least 4 MiB in size).<\/span><\/p>\n<p><span>In essence, we are taking the memory previously used to hold the content of ArrayBuffer C and get Hermes to reuse it for a JS heap segment (remember the dotted section of our earlier diagram?). While doing so, we are maintaining read\/write (RW) access to the segment via B\u2019.<\/span><\/p>\n\n<p><span>Now we can overwrite the content of the segment S from B\u2019.<\/span><\/p>\n<p><span>GC segments contain plenty of JS object instances, including ArrayBuffer objects. If we spray enough ArrayBuffers, some of them will be placed in the new segment:<\/span><\/p>\n\n<p><span>To summarize, we got Hermes to reuse C\u2019s memory to host the \u201cmetadata\u201d of several other ArrayBuffers. This was possible because ArrayBuffers\u2019 contents and JS object metadata are both ultimately backed by mmap (see our earlier diagram).<\/span><\/p>\n<p><span>Each ArrayBuffer object contains a pointer to the underlying data buffer (i.e., its content).<\/span><\/p>\n<p><span>Using B\u2019, we can change the address in a chosen instance and make it point anywhere in memory instead of to their original buffer (D in the image). Then, by reading or writing to that ArrayBuffer instance, we can read or write at any arbitrary address!<\/span><\/p>\n<h3>Putting it all together<\/h3>\n<p><span>We came a long way from a mysterious crash in <\/span><span>Array.prototype.sort<\/span><span> to obtaining unlimited R\/W capabilities.<\/span><\/p>\n<p><span>To summarize:<\/span><\/p>\n<p><span>We created a strategic memory layout of four adjacent mmap\u2019d buffers.<\/span><br \/>\n<span>We used the sorting bug to swap the size of a malloc chunk and obtain two overlapping ArrayBuffers.<\/span><br \/>\n<span>Finally, we replaced one of the overlapping buffers with a GC segment so that we could overwrite pointers and obtain arbitrary R\/W access.<\/span><\/p>\n<p><span>A typical exploit flow would continue as follows:<\/span><\/p>\n<p><span>Write an<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Return-oriented_programming\" target=\"_blank\" rel=\"noopener\"> <span>ROP-chain<\/span><\/a><span> and<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Shellcode\" target=\"_blank\" rel=\"noopener\"> <span>shellcode<\/span><\/a><span> somewhere in memory.<\/span><br \/>\n<span>Leak some addresses to defeat<\/span><a href=\"https:\/\/en.wikipedia.org\/wiki\/Address_space_layout_randomization\"> <span>ASLR<\/span><\/a><span>.<\/span><br \/>\n<span>Overwrite function pointer with a<\/span><a href=\"https:\/\/failingsilently.wordpress.com\/2018\/04\/17\/what-is-a-stack-pivot\/\" target=\"_blank\" rel=\"noopener\"> <span>stack pivoting gadget<\/span><\/a><span>.<\/span><br \/>\n<span>Execute ROP-chain and make shellcode executable.<\/span><br \/>\n<span>Jump to shellcode.<\/span><br \/>\n<span>Profit.<\/span><\/p>\n<p><span>We will not cover the details of the remaining steps, as they refer to well-known exploitation techniques. The final result lets us execute arbitrary code directly from Hermes and escape the JS sandbox.<\/span><\/p>\n<p><span>To make things more fun, we used our exploit to run <\/span><span>Doom<\/span><span>:<\/span><\/p>\n<div class=\"wp-video\"><!--[if lt IE 9]&gt;document.createElement('video');&lt;![endif]--><br \/>\n<a href=\"https:\/\/engineering.fb.com\/wp-content\/uploads\/2022\/07\/Hermes-Quicksort-Doom.mp4\">https:\/\/engineering.fb.com\/wp-content\/uploads\/2022\/07\/Hermes-Quicksort-Doom.mp4<\/a><\/div>\n<h2>Conclusion<\/h2>\n<p><span>Thanks to our bug bounty researcher\u2019s quick find and report (within six days of the bug being introduced!) and the team\u2019s work on it, this bug wasn\u2019t included in any<\/span><a href=\"https:\/\/github.com\/facebook\/hermes\/releases\" target=\"_blank\" rel=\"noopener\"> <span>Hermes public releases<\/span><\/a><span>.<\/span><\/p>\n<p><span>Based on our internal investigation, we awarded the researcher with a $12,000 bounty. While we always perform an in-depth investigation of each bug submission, we strongly encourage researchers to dive deeper in our native products. On that note, we<\/span><a href=\"https:\/\/www.facebook.com\/notes\/872652486869007\/\" target=\"_blank\" rel=\"noopener\"> <span>award bonuses<\/span><\/a><span> to reports that feature a full proof of concept.<\/span><\/p>\n<p><span>This and other reports were submitted within the context of our <\/span><a href=\"https:\/\/www.facebook.com\/notes\/685647469051301\" target=\"_blank\" rel=\"noopener\"><span>Hermes and SparkAR bug bounty<\/span><\/a><span> track, which incentivizes security research on these products.<\/span><\/p>\n\n<p><span>Since this bug was reported, we have improved our internal fuzzing effort to help find bugs earlier on, and we are currently working on hardening measures that would drastically diminish the impact of similar bugs.<\/span><\/p>\n<p><span>We encourage researchers to share their findings and work with our internal research team to test our mitigations so we can keep improving our defenses. We\u2019ll continue to share notable bugs and <a href=\"https:\/\/engineering.fb.com\/2022\/07\/20\/security\/how-meta-and-the-security-industry-collaborate-to-secure-the-internet\/\" target=\"_blank\" rel=\"noopener\">bug bounty reports<\/a> to celebrate the work of our community. Happy hunting!<\/span><\/p>\n<p>The post <a href=\"https:\/\/engineering.fb.com\/2022\/07\/20\/security\/hermes-quicksort-to-run-doom\/\">Using Hermes\u2019s Quicksort to run Doom: A tale of JavaScript exploitation<\/a> appeared first on <a href=\"https:\/\/engineering.fb.com\/\">Engineering at Meta<\/a>.<\/p>\n<p>Engineering at Meta<\/p>","protected":false},"excerpt":{"rendered":"<p>At Meta, our Bug Bounty program is an important element of our \u201cdefense-in-depth\u201d approach to security. Our internal product security teams investigate every bug submission to assess its maximum potential impact so that we can always reward external researchers based on both the bug they found and our further internal research assessment of where else&hellip; <a class=\"more-link\" href=\"https:\/\/fde.cat\/index.php\/2022\/07\/20\/using-hermess-quicksort-to-run-doom-a-tale-of-javascript-exploitation\/\">Continue reading <span class=\"screen-reader-text\">Using Hermes\u2019s Quicksort to run Doom: A tale of JavaScript exploitation<\/span><\/a><\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"spay_email":"","footnotes":""},"categories":[7],"tags":[],"class_list":["post-609","post","type-post","status-publish","format-standard","hentry","category-technology","entry"],"jetpack_featured_media_url":"","jetpack-related-posts":[{"id":610,"url":"https:\/\/fde.cat\/index.php\/2022\/07\/20\/how-meta-and-the-security-industry-collaborate-to-secure-the-internet\/","url_meta":{"origin":609,"position":0},"title":"How Meta and the security industry collaborate to secure the internet","date":"July 20, 2022","format":false,"excerpt":"Bug hunting is hard and can sometimes go unnoticed across our industry. Building scalable bug detection methods across large codebases and open source libraries is an underappreciated yet critical effort every engineering company has to work through. Because the ideal outcome is that bugs are found and fixed before they\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":516,"url":"https:\/\/fde.cat\/index.php\/2021\/12\/15\/charting-the-future-of-our-bug-bounty-program\/","url_meta":{"origin":609,"position":1},"title":"Charting the future of our bug bounty program","date":"December 15, 2021","format":false,"excerpt":"We\u2019re tackling the industry-wide issue of scraping by expanding our bug bounty program to reward valid reports of scraping bugs and unprotected data sets. To the best of our knowledge, this is an industry first.\u00a0 Looking toward the future, we\u2019re also launching new educational opportunities for researchers and hosting our\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":263,"url":"https:\/\/fde.cat\/index.php\/2021\/08\/31\/minesweeper-automates-root-cause-analysis-as-a-first-line-defense-against-bugs\/","url_meta":{"origin":609,"position":2},"title":"Minesweeper automates root cause analysis as a first-line defense against bugs","date":"August 31, 2021","format":false,"excerpt":"Root cause analysis (RCA) is an important part of fixing any bug. After all, you can\u2019t solve a problem without getting to the heart of it. But RCA isn\u2019t always simple, especially at a scale like Facebook\u2019s. When billions of people are using an app on a variety of platforms\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":720,"url":"https:\/\/fde.cat\/index.php\/2023\/05\/29\/sre-weekly-issue-374\/","url_meta":{"origin":609,"position":3},"title":"SRE Weekly Issue #374","date":"May 29, 2023","format":false,"excerpt":"View on sreweekly.com A message from our sponsor, Rootly: Rootly is hiring for a Sr. Developer Relations Advocate to continue helping more world-class companies like Figma, NVIDIA, Squarespace, accelerate their incident management journey. Looking for previous on-call engineers with a passion for making the world a more reliable place. Learn\u2026","rel":"","context":"In &quot;SRE&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":492,"url":"https:\/\/fde.cat\/index.php\/2021\/10\/20\/facebook-engineers-receive-2021-ieee-computer-society-cybersecurity-award-for-static-analysis-tools\/","url_meta":{"origin":609,"position":4},"title":"Facebook engineers receive 2021 IEEE Computer Society Cybersecurity Award for static analysis tools","date":"October 20, 2021","format":false,"excerpt":"Until recently, static analysis tools weren\u2019t seen by our industry as a reliable element of securing code at scale. After nearly a decade of investing in refining these systems, I\u2019m so proud to celebrate our engineering teams today for being awarded the IEEE Computer Society\u2019s Cybersecurity Award for Practice for\u2026","rel":"","context":"In &quot;Technology&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":519,"url":"https:\/\/fde.cat\/index.php\/2021\/12\/20\/sre-weekly-issue-301\/","url_meta":{"origin":609,"position":5},"title":"SRE Weekly Issue #301","date":"December 20, 2021","format":false,"excerpt":"View on sreweekly.com A message from our sponsor, Rootly: Manage incidents directly from Slack with Rootly \ud83d\ude92. Automate manual admin tasks like creating incident channel, Jira and Zoom, paging the right team, postmortem timeline, setting up reminders, and more. Book a demo: https:\/\/rootly.com\/demo\/?utm_source=sreweekly Articles BadgerDAO Exploit Technical Post Mortem This\u2026","rel":"","context":"In &quot;SRE&quot;","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"_links":{"self":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts\/609","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/comments?post=609"}],"version-history":[{"count":0,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/posts\/609\/revisions"}],"wp:attachment":[{"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/media?parent=609"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/categories?post=609"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fde.cat\/index.php\/wp-json\/wp\/v2\/tags?post=609"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}