In my previous post, I described how the history of JavaScript has led to the mushrooming complexity – and corresponding attack surface – of modern JavaScript engines. Judging from submissions to the Zero Day Initiative (ZDI), the JavaScript engine is the principal epicenter of browser vulnerabilities today, both in quantity and in quality. In this post, I will present details of a prime example of a vulnerability within the execution engine of Chakra, the JavaScript engine present in Microsoft Edge. This will be a deep dive, so grab a beverage! Show At the 2017 Pwn2Own competition, among the victorious contestants was Tencent Security – Team Ether. On Day 1 of the competition, they delighted everyone with a remote code execution and sandbox escape exploit chain on Microsoft Edge. They gained remote code execution through a bug in Chakra, CVE-2017-0234. The proof-of-concept trigger for this bug is a tiny and innocent-looking snippet of JavaScript: Figure 1 - CVE-2017-0234 PoC (Click to enlarge) The
Figure 2 - CVE-2017-0234 PoC Debugger Output (Click to enlarge) The out-of-bounds write occurs in the JIT-compiled code. A quick examination of the code reveals the problem: Figure 3 - CVE-2017-0234 Faulty JIT Code (Click to enlarge) Amazingly, the JIT compiler has produced code that is completely missing a bounds check! Why would that ever occur? Some Background on JavaScript Execution Before exploring the answer, some essential background information is in order. As I mentioned in the previous post, modern JIT engines cannot rely entirely on either interpreted or compiled execution. Interpreted execution is too slow for high-performance scenarios, and compiled execution is prohibitive in terms of up-front startup delay. Furthermore, it is infeasible to directly compile JavaScript into high-performing native code because of the extreme dynamic nature of the language. To solve these problems, modern JavaScript engines employ multiple modes of execution. When new script is loaded, the engine begins executing it with an interpreter, enabling rapid startup. In addition to executing the script, the interpreter is also tasked with acquiring dynamic profiling information. This includes execution counts for functions and loops, as well as information such as the types found in individual variables. At some point, the engine may determine that it is advantageous to compile some or all of the script. It makes this decision based upon the profiling counters updated during interpretation. When this occurs, the compiler makes use of the dynamic profiling information collected during interpretation to guide the compilation process. This on-the-fly compilation is also known as “JIT” or just-in-time compilation. As an example, suppose a line of JavaScript makes
an assignment to Note that in the JIT code in Figure 3, two such “bail out” branches are seen. The first checks that Figure 4 - CVE-2017-0234 JIT Code: Branch to Bailout (Click to enlarge) Analyzing the Patch for CVE-2017-0234 When I began analyzing this vulnerability, Microsoft had already shipped a patch. Searching the ChakraCore repository for commit messages containing “CVE-2017-0234” immediately revealed the commit containing the patch. The patch ( Figure 5 - CVE-2017-0234 Patch (Click to enlarge) We can see immediately
that the problem was that, prior to the patch, Let’s analyze the patched version of the code and see if we can understand why it is safe to remove bounds checks when all the specified conditions hold true.
Figure 6 - Conditions for Bounds Check Elimination (Click to enlarge) Starting with condition 1 in the figure above: The variable Proceeding to condition 2: This condition determines whether any additional actions will be needed at runtime in the event that script attempts to access an index past the end of the array. If the operation is a write into an element ( Condition 3 checks whether we are in an asm.js function. For the purposes of this discussion, we will assume we are not executing asm.js. In condition 4, the code looks for the array index and obtains a pointer to a corresponding Finally, in condition 5, the code interrogates the
Understanding the Upper Bound Check What is quite curious is the last line in condition 5 where the upper bound is examined. One might expect it to ensure that the upper bound for the index is less than the array length. But that is not what it does at all! Instead, it only ensures that the upper bound for the index (when multiplied by the element size, as encoded in To illustrate, consider the following script: Figure 7 - PoC to Achieve Bounds Check Elimination, Post-Patch (Click to enlarge) When compiling the script in Figure 7, the compiler recognizes that when execution reaches the array access, the script variable index is guaranteed to be between 0 and 0x40000000 (exclusive of 0x40000000). This information will be
reflected in Figure 8 - Out-of-Bounds Access, Post-Patch (Click to enlarge) It appears that we have circumvented the patch, and the patch has failed. However, in this case, appearances are quite deceiving. The key to the puzzle is found back in condition 1 in Figure 6. Condition 1 ensures that bounds check elimination will not be performed unless In ArrayBuffer.cpp: Figure 9 - JavascriptArrayBuffer Constructor (Click to enlarge) Figure 10 - AsmJsVirtualAllocator (Click to enlarge) “The amount of memory reserved is unrelated to the amount of memory requested.” If the requested buffer length meets the conditions for a “virtual” buffer, allocation is performed by the This surprising and seemingly wasteful behavior provides a great benefit: When accessing a virtual array, JIT code can add any 32-bit displacement to the buffer base address and access the resulting address safely,
without performing any bounds check. If the resulting address is past the end of the buffer, it will fall within the reserved but non-committed region. Recognizing the invalid address, the processor’s MMU will generate an access violation fault. The Chakra engine will then catch the access violation, recognize that it came from JIT code, and resume execution with the next instruction (see In the above example, after the AV occurs in the debugger, if we run Figure 11 - Invalid Address is Reserved, not Free (Click to enlarge) “Put another way, for virtual arrays bounds checking is offloaded from software to the hardware MMU.” We can now understand the logic of condition 5, which checks if it can be guaranteed that the index will not result in a displacement greater than or equal to 2^32. If this cannot be guaranteed, software bounds checks are needed. But if the compiler can make a guarantee based on static examination of the code that the displacement will always be less than 2^32 (as is the case for the script of Figure 7), software bounds checks can be omitted. Any out-of-bounds condition will be turned safely into an AV by the MMU. I speculate that when the original pre-patch code was written, the author failed to notice that when the 32-bit index is scaled up by the element size, the resulting displacement may be larger than a 32-bit unsigned integer. Ensuring an Array is Virtual: Array Type Checking This is all very well when the buffer has been allocated using the “virtual” strategy. If the buffer has been allocated in the traditional way, however, it is unsafe to omit software bounds checks. As a result, condition 1 above warrants a closer look. The method used by condition 1 is Figure 12 - Array Type Check Insertion Logic (Click to enlarge) Recall that The answer to this riddle lies in the details of the If Circumstances When Array Type Checking Can Be Omitted As a follow-up, I was curious as to what circumstances could lead to Investigating this question, I noticed the method Figure 13 - Multiple Array Accesses Requiring Only a Single Type Check (Click to enlarge) For the first line, A Harmful Security Effect of Hardware Bounds Checking As we have discussed, Chakra boosts performance of bounds checks on certain large arrays by offloading the checks from software to hardware. It achieves this by reserving sufficiently large regions of reserved memory so that any out-of-bounds condition will result in an access of reserved, non-committed memory, producing an access violation. Accordingly, the Chakra engine recognizes that access violations originating from JIT code are expected occurrences and continues script execution in accordance with the ECMAScript specification. Unfortunately, this has a somewhat negative effect on the security posture of the process as a whole. In traditionally-coded processes that do not use hardware bounds checking, any access violation that arises is a clear sign of corruption. In most applications, there is no reason to attempt to recover from an access violation. Instead, applications allow the process to terminate immediately. This default behavior is positive from a security standpoint. It creates a certain natural impediment for exploitation: An exploit can be successful only if it runs to completion without causing corruption that triggers an access violation. There is no second chance. Access violations produced within JIT-compiled code, however, do not result in process termination. In fact, Chakra even allows JavaScript execution to proceed. This gives exploitation attempts an unusual amount of leeway. Even an unreliable exploitation technique, which on a traditional process would have little chance of succeeding before shutdown occurs, will be given the opportunity to make an unlimited number of attempts on Chakra. It even may be possible for an exploit to abuse this specifically to probe and discover valid memory addresses. Microsoft has largely mitigated this deficiency in a recent commit of ChakraCore ( Summary In this post, we’ve explored some of the complexities of the Chakra JIT compiler, specifically regarding enforcement of bounds checks in native JIT code. One notable feature we’ve uncovered is that the compiler will sometimes omit bounds checking instructions entirely, instead relying on large regions of reserved address space to provide a hardware-enforced safety net to catch attempted out-of-bounds accesses. Although the approach is fundamentally sound, there is much opportunity for subtle errors, as is demonstrated by CVE-2017-0234. In truth, we have only scratched the surface of the complex logic employed by the JIT compiler and the enormous variety of scenarios it must properly handle. We expect that Chakra and other JavaScript engines will continue to be a fertile area for vulnerability research. You can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches.
What is bound checking in array?Array bound checking refers to determining whether all array references in a program are within their declared ranges. This checking is critical for software verification and validation because subscripting arrays beyond their declared sizes may produce unexpected results, security holes, or failures.
Does compiler perform bounds checking while array elements?Since the array is constructed as the program is running, the compiler does not know its length and can't detect some errors. As a Java program is running, each time an array index is used it is checked to be sure that it is OK. This is called bounds checking, and is extremely important for catching errors.
What happens when trying to access an array element using an out of bounds index?If we use an array index that is out of bounds, then the compiler will probably compile and even run. But, there is no guarantee to get the correct result. Result may unpredictable and it will start causing many problems that will be hard to find. Therefore, you must be careful while using array indexing.
Does C++ provide bounds checking on arrays?C++ performs no bounds checking on arrays; nothing stops you from overrunning the end of an array. If this happens during an assignment operation, you will be assigning values to some other variable's data.
|