Vulnerability Detection Overview
When attempting to write network detection for exploits, several factors must be considered, some of which only apply due to ET having a diverse customer base with varying network configurations, appliance/hardware specifications, and unique environments. Detection coverage for various vulnerability classes can often be categorized into:
• Easy to cover without a severe impact on performance
• Hard to assign a dedicated signature
• Hard to cover without a severe impact on performance
• Impossible to cover reliably
In this post I will address memory corruption exploits, Suricata/Snort engine limitations around memory corruption exploits, and a concept for detection that I have derived from years of analysing memory corruption exploits, receiving endless requests for coverage of such exploits, and attempting to build network-based detections for them.
Categorising
Exploits with a high probability of being ‘impossible to cover’ often come in the form of use-after-free, out of bounds read/write, type confusion, and other memory corruption exploits in the context of a browser - especially when the vulnerability resides within a JavaScript engine. Google Chrome/Chromium uses an engine called V8, an open-source JavaScript and WebAssembly engine written in C++. Microsoft Edge also uses V8 but previously used the Chakra engine (ChakraCore) in IE/Edge (EOL March 2021) which supports JScript (Microsoft’s JavaScript implementation). Firefox uses Mozilla SpiderMonkey etc. All these engines are open source if you wish to dive further into JavaScript engine internals.
Exploits often considered to be ‘hard to cover without a severe impact on performance’ simply refer to exploits that do not provide sufficient content matching opportunities to push the signature into a comfortable place in terms of performance. This is why we may not label a detection as covering every case of a CVE or every possible outcome of an exploit. A common contender for this category is a stack/buffer/integer overflow, especially when there is seemingly no packet structure or the packet structure is not publicly documented because it is a proprietary protocol or other reasons. Sometimes, you may find a browser-based exploit that fits into this category if the trigger is relatively static however this is quite rare.
Assigning dedicated signatures for each vulnerability seems obvious however, there are scenarios in which no dedicated signature is available. Typically, this is due to a lack of content matching opportunities combined with generic/reusable content used to trigger a vulnerability. A prime example for this category is Java/.NET deserialization, but only in certain applications. For example you may have a Java deserialization vulnerability in a parameter of a HTTP request, making it relatively easy to identify the affected product while easily positioning the detection logic for the deserialization within network packets. In other instances, you may find that a Java deserialization exploit is being sent to a TCP socket and the packet solely consists of a serialized object, drastically limiting detection opportunities and pushing coverage towards relying on the usage of tools such as ysoserial. Another class of vulnerability which often fits into this category is stack/buffer/integer overflows, depending on the vulnerable application.
Finally, we have ‘easy to cover without a severe impact on performance’. Vulnerability classes including but not limited to command injection, server-side request forgery, SQL injection, use of hard-coded credentials, directory traversal, LFI/RFI etc. are common in this category due to most of them sharing a characteristic or having similar characteristics that provide excellent, unique content matching opportunities:
• Specific parameters that are required for a vulnerability to trigger.
• Specific characters used to escape an application and pass injections resulting in code/command execution.
Detecting Use-After-Free Exploits: CVE-2021-1879 Case Study
Use-After-Free (UAF) vulnerabilities, especially in browser contexts, present significant challenges for network-based detection. In this section, I will briefly focus on CVE-2021-1879, a UAF vulnerability in QuickTimePluginReplacement
and explain an old detection approach that I consider to be falling short of what is required or expected for CVE detection.
Exploit Overview
In short, this vulnerability involves referencing external JSValues
in m_scriptObject
, which allows a UAF condition to be triggered when the garbage collector removes these JSValues
but they are later accessed. The proof-of-concept (POC) code below highlights this exploit in action:
var worker = null;
function start() {
worker = document.getElementById("worker");
window.top.document.getElementById("cur").addEventListener("DOMNodeInserted", callback0);
var intl = setInterval(function(){
worker.GetURL.a = 777;
window.top.f();
clearInterval(intl);
}, 1);
}
function callback0(ev) {
window.requestAnimationFrame(callback);
}
function gc() {
for (let i = 0; i < 0x40; i++) { new ArrayBuffer(0x1000000); }
}
function callback(ev) {
gc();
}
In this code, several elements stand out and when combined, initially seem like good detection candidates:
- Worker Assignment and DOM Manipulation - The worker variable is initially set to null and later assigned a DOM element with
getElementById
. TheaddEventListener
method monitors DOM changes, specificallyDOMNodeInserted
, which triggers a callback. - Garbage Collection (GC) Trigger - The
gc()
function is invoked in a loop, allocating memory repeatedly usingArrayBuffer
. This effectively forces garbage collection, potentially freeing memory that still has active references. - Access After Free - After triggering GC, the code references the
worker.GetURL.a
property, leading to a UAF condition. This access after the object is freed is critical for exploitation.
Detection Challenges and Strategy
Detecting UAF exploitation, particularly in JavaScript engines (JSEs), is difficult due to the dynamic nature of these exploits. Static detection is not feasible for all cases because variable names and values are often obfuscated or dynamically generated. Additionally, even if static content detection was considered satisfactory for detection, we still run into the issue of not knowing the memory state of the machine or the types of certain variables (in the context of Type Confusion vulnerabilities).
Unfortunately, Suricata/Snort do not contain features powerful enough to accurately detect these exploits on a level granular enough for us to tag them as specific CVEs. This is absolutely expected and not a fault of Suricata/Snort, ultimately this is a host-based problem at the core but it does not stop us from trying.
The following PCRE attempts to match the key elements in the PoC:
var\s*(?P<worker>[A-Za-z0-9_-]{1,20})\s*=\s*null\x3b.{1,300}(?P=worker)\s*=\s*document
\.getElementById\(\x22(?P=worker)\x22\)\x3b.{1,300}\.addEventListener\(\x22
DOMNodeInserted\x22\s*,\s*(?P<callback0>[A-Za-z0-9_-]{1,20}).{0,300}(?P=worker)
(?P<worker_ext>(\.\w{1,20})+)\s*=\s*\d+\x3b.{1,300}function\s*(?P=callback0)\([^\)]+\)
\s*\{\s*.{1,300}\.requestAnimationFrame\((?P<callback>[A-Za-z0-9_-]{1,20})\)\x3b
.{1,300}function\s*(?P<garbagecollector>[A-Za-z0-9_-]{1,20})\(\)\s*\{\s*.{0,100}for
\s*\(let\s*(?P<gc_counter>[A-Za-z0-9_-]{1,20})\s*=\s*\d{1,8}\s*\x3b\s*(?P=gc_counter)
\s*(?:<|>)\s*(?:0x)?\d{2,}\s*\x3b\s*(?P=gc_counter)(?:\+{2}|-{2})\s*\)\s*.{1,300}
function\s*(?P=callback)\([^\)]+\)\s*\{\s*.{1,300}(?P=garbagecollector)\(\)\s*\x3b
\s*.{1,300}\((?P=worker)(?P=worker_ext)\)
Yes, it is very ugly and the performance is far from desirable. We are attempting to isolate certain variables of interest into their own capture groups so that we may reference them later on when they become relevant. Essentially this means that for a specific POC, we can track variables of interest and identify when they are utilised in the context of the POC to trigger a vulnerability. As you can imagine, this is still awfully static and still way too specific to the POC.
The Concept of JIT Forcing
Just-In-Time (JIT) compilation is a feature in modern JavaScript engines (JSEs) that dynamically compiles JavaScript code into machine code at runtime, optimizing performance. However, JIT compilation is a double-edged sword: while it improves efficiency, it can also be exploited by attackers. In the context of exploitation, JIT forcing refers to techniques that intentionally trigger the JIT compiler to allocate and optimize specific chunks of memory. By doing so, attackers can manipulate the memory layout, increasing the chances of successfully exploiting vulnerabilities like Use-After-Free (UAF) and Type Confusion.
In many POCs for JavaScript exploits, JIT forcing is a critical step. Attackers often allocate large blocks of memory in a loop, as seen in the gc()
(garbage collection) function of the PoC for CVE-2021-1879 shown previously:
function gc() {
for (let i = 0; i < 0x40; i++) {
new ArrayBuffer(0x1000000);
}
}
The repeated creation of ArrayBuffer
objects fills memory with garbage data, forcing the JIT compiler to optimize the code. This manipulation of memory allocation can create conditions favourable for exploitation, such as placing sensitive objects in predictable memory locations. Once the JIT compiler optimizes the memory layout, attackers can leverage vulnerabilities like UAF to gain control over execution flow.
Why Focus on JIT Forcing?
Detecting JIT forcing provides a practical approach for network-based detection systems like Suricata and Snort, especially when identifying highly dynamic and obfuscated exploits. Unlike static indicators, JIT-related behaviour tends to be more consistent across different POC exploits and real-world attacks. This consistency makes it a reliable and effective detection method at the cost of some performance.
The primary reasons why JIT forcing is an effective detection method and the reason I have opted to pursue this method is its consistency across various exploits. While specific exploit payloads can differ significantly, the underlying behavior of allocating large memory blocks in loops is common in many JavaScript engine memory corruption exploits. This makes JIT forcing a dependable heuristic for detection in diverse attack scenarios.
Additionally, many exploits targeting JIT compilers depend on predictable engine behavior. By monitoring unusual or excessive memory allocations, detection systems can infer that JIT optimization is being manipulated to facilitate an attack. Identifying these anomalies can help detect and prevent exploitation attempts before they succeed.
Detection Strategy for JIT Forcing
Given the dynamic nature of JIT exploits, a hunting-based approach rather than strict signature matching is recommended. This involves monitoring for suspicious patterns matching specific behaviours, such as identifying loops that repeatedly allocate significant amounts of memory, especially using constructs like ArrayBuffer, Uint8Array, or similar objects, or actions such as repeatedly calling a function.
Here are some regex patterns we have included in our JIT Forcing Suricata rules to achieve our goal of JSE exploitation detection:
\s*(?P<count_var>[\w\-]{1,20})\s*=\s*(0x[a-f0-9]{3,12}|\d{4,12}).{1,500}function\s*
(?P<jit_func>[\w\-]{1,30})\(.{1,500}for\s*\(let\s*(?P<counter>[\w\-]{1,20})\s*=\s*
\d+\s*\x3b\s*(?P=counter)\s*<\s*(?P=count_var)\s*\x3b\s*(?P=counter)\+{2}\s*\)
.{1,100}(?P=jit_func)\(
Heavily utilising regex in this context means we can identify ‘for’ loops that allocate large memory chunks using various array types and other JIT-forcing related behaviours such as repeatedly calling a function. Of course there are almost endless methods of achieving the same behaviour but POCs we have observed are often very similar in their JIT forcing implementation, sparking this detection approach.
One question you may have is “surely the false positives are insane here?” and honestly, the FP rates are surprisingly low. There have been certain patterns that we made too ‘loose’ which has caused a spike in false positives but they were very easy to reel in. Due to the bizarre and obscure behaviour described in the pattern, it seems rather difficult to trigger an alert with benign/legitimate JavaScript.
One of the challenges we face with detection in this context is avoiding reliance on POC coverage alone, as you have just seen. We do not want to give the impression we have total coverage, but rather that we have crafted detection logic to meet criteria we have observed in the wild and through investigation. While POC signatures can provide initial detection capabilities, attackers often modify their payloads to evade these detections. By focusing on behavioural patterns like JIT forcing, we can create more adaptive and resilient signatures.
A Chrome Extension?
A quick story time to close things out here.
Recently, Stuart (stu4rt on Discourse) joined Emerging Threats and showed interest in my JIT forcing concept. As a joke (somewhat), I suggested developing a Chrome extension that could scan inbound JS files during usual web browsing against the regex patterns we have incorporated into the Suricata/Snort rulesets. The goal would be to gather benign/legitimate JS files to build a known-good corpus so we can adjust our patterns to prevent false positives and disruption. Sure enough, Stuart put that Chrome extension together overnight and we will be making that available publicly soon.
Since we are releasing the Chrome extension, we will also be pushing our JIT-Forcing Suricata/Snort rules to the OPEN ruleset (when the extension is released) for anybody to use and critique, we love feedback so please, do not hold back.
Rules
2058051 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M1 (hunting.rules)
2058052 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M2 (hunting.rules)
2058053 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M3 (hunting.rules)
2058054 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M4 (hunting.rules)
2058055 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M5 (hunting.rules)
2058056 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M4 (hunting.rules)
2058057 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M6 (hunting.rules)
2058058 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M7 (hunting.rules)
2058059 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M8 (hunting.rules)
2058060 - ET HUNTING JavaScript Engine JIT Forcing Observed - Investigate Possible Exploitation M9 (hunting.rules)