취약점 연구
엔키화이트햇
Jun 3, 2024
Introduction
In the past, major JavaScript engines such as Google V8 and Apple JavaScriptCore were plagued by numerous side-effect bugs in their JIT compilers. Google’s V8 Typer system and Apple’s AbstractInterpreter(AI) were particularly problematic. However, thanks to extensive efforts to fix these problems, finding such vulnerabilities has become much more challenging.
While looking for a WebKit security update, we came across an interesting bug case related to AI. It has also been reported as being exploited in the wild.
Although it’s quite an old bug and now that Apple is preparing iOS 18, We thought this was a case that browser security researchers should take a look at, we share a detailed analysis of this case.
Safari RCE bug
There is Rapid Security Update for iOS 16.5.1 which is known as CVE-2023–37450. You can see the information from here.
https://support.apple.com/en-us/HT213841
We’re not sure if this is exactly the patch above, but it’s also an exploitable RCE bug. You can see the patch commits from here.
https://github.com/WebKit/WebKit/commit/1b0741f400ee2d31931ae30f2ddebe66e8fb0945
https://github.com/WebKit/WebKit/commit/39476b8c83f0ac6c9a06582e4d8e5aef0bb0a88f
FIX
At the moment where we analyze the issue before, we couldn’t find matching CVE issue.
Therefore as we said like above, we guessed this might be CVE-2023–37450, but it’s turned out it’s not (Thank you for letting us know @krzywix).
We still don’t know what was it, but it’s absolutely worth looking at.
Root cause analysis
The patch commit itself quite nicely explains the bug itself. It’s about the slow/fast path for property access mechanism which is quite common vulnerable pattern for JS engine.
Let us explain with an example.
In the above example, o.p1
doesn't have any security problem. But we can add some custom handler for property access.
Unlike the first example, when we access to p1
, we can invoke custom handler to get the property value. So why is this harmful for JS engine's security? Because it can violate JS engine's assumption for both Runtime/JIT context. The bible for this kind of vulnerability is CVE-2016-4622.
Anyway, now move our focus to the patch log.
Not that hard to understand. There are a few points to notice.
They add indexed accessor check for global object.
They prevent for defineProperty to configure Array and Function type’s
length
orprototype
property.
So, based on patch log, attacker can break JS engine’s assumption that some properties like above things are unconfigurable. But through the bug, we can make them configurable.
Before diving into how this is possible, there is very simple poc for this.
According to MDN, super
is defined like following.
When executing super.prototype = 1
, it's handled by slow_path_put_by_id_with_this
and calls JSObject::putInlineSlow
.
When put property into JSObject, it checks several things.
At [1], it checks whether property exists in current JSObject scope.
PropertyOffset
is returned if exist. JSC's JSEngine stores property information instructure
object. There are 2 important members instructure
object -m_seenProperties
,m_propertyTableUnsafe
. If it returns validPropertyOffset
at [2], it checks current offset's attributes likeCustomAccessor
,ReadOnly
and etc.If property does not exist in property table, it checks whether current property comes from static property table at [3]. You can see easy example in following link.
At [4], if above cases fail, it traverses JSObject’s prototype chain, and starts property lookup from [1].
If it fails to find property, then it tries to define property as new one. At [5], based on result of isThisValueAltered
, it calls definePropertyOnReceiver
or putInlineFast
.
Now we should remind that before entering JSObject::putInlineSlow
, JSC Runtime creates some base JSValue in slow_path_put_by_id_with_this
. These are very important in isThisValueAltered
, so we should know each JSValue.
baseValue
thisVal
putValue
baseValue
stands for Function.prototype
or Function.__proto__
. thisValue
stands for this
in constructor context. putValue
is 1
in our example.
Therefore, as thisValue
and baseObject
are different, isThisValueAltered
returns true, we fallthrough definePropertyOnReceiver
. In JSObject::definePropertyOnReceiver
, as similar to property lookup routine, it checks several things to take slow path.
Check that type of receiver
is GlobalProxyType
to get the correct receiver
.
Check whether the
receiver
has any kinds ofGetterSetter
.
None of them are matched, now we reach fast path to put property. Since we don’t have any static property table, JSObject::putInlineFast
is called.
In JSObject::putDirectInternal
, as it adds prototype
property to OOL (Out of line) property named butterfly, JSC engine trigger structure transition for this
JSObject and fire StructureTransitionWatchpoint
. You can see the details for concept about WatchPoint
in the offical WebKit blog!
So, normally prototype
property is unconfigurable, but through this way, we make prototype
property as OOL property which is configurable.
But the main question is still unclear.
Why is this exploitable?
To answer this question, we should find out what happens if something that was assumed to be non-configurable is now actually configurable.
And in test cases, test1
is very interesting.
When we define Getter
for our custom Function object, it calls JSFunction::getOwnPropertySlot
through the following backtrace.
Basically, since prototype
is an unconfigurable property, it shouldn't have a valid property offset, but due to the bug, it has.
It calls slot.setValue(...)
, and this function directly sets propertySlot
. The statement f.__defineGetter__("prototype", () => {});
is GetterSetter
, but it doesn't fire any watchpoints and is treated like normal JSValue.
To execute our Getter
, we should reach PropertySlot::getValue
.
To make side effect do their job in optimized code in JSC, we should make sure that DFG (one of JSC’s JIT compiler)’s AI (Abstract Intepreter) thinks it’s safe to execute. The AI mostly works on CFA (Control Flow Analysis) phase in JIT compiler. When DFG’s AI thinks it’s not safe to execute (actually means not safe to optimize), it calls clobberWorld()
. Since AI is a type of state machine, when clobberWorld()
is called, AI's state is changed to ClobberedStructures
. And this information will be used in Constant Folding phase and also indirectly used in several phases like CSE (Common Subexpression Elimination) phase.
Searching for a while, it seems that Spread
opcode is quite interesting as it can access to all elements when it creates JSImmutableButterfly
. Also Spread
opcode is used in AI analysis phase, and it will be determined as safe node.
Following is type confusion poc for this bug. Tested on WebKit commit c7d1888949f94118612536ffc3b7f58cf102114b.
Conclusion
It was very interesting traditional side-effect bug and getting arbitrary read/write primitive from there is not that hard task.
However, hijacking the control flow of the WebContent process on macOS and iOS requires bypassing several mitigations.
[Image from https://www.synacktiv.com/sites/default/files/2022-10/attacking_safari_in_2022_slides.pdf]
As illustrated, Apple has introduced numerous hardware and software-based mitigations.
Among these mitigations, two stand out as particularly challenging:
PAC (Pointer Authentication Code) bypass
JIT Cage bypass
The PAC is a hardware-based mitigation introduced by ARMv8.3 in 2016 to protect sensitive pointers, such as virtual function tables. It signs pointers with secret keys and authenticates the signature before accessing it. Since the A12 processor on iPhone and M1 on macOS, PAC is the default for Apple platform binaries such as Safari. Therefore, a PAC bypass is usually required to hijack control flow.
There are interested recent usermode PAC bypass for WebContent process in public.
Manfred Paul (@_manfp) utilized it for his Pwn2Own 2024 Safari exploit https://github.com/WebKit/WebKit/commit/81c26e6a4483686853f4f88dbde6e212062755d3
Synacktiv released their offensivecon 2024 slide, there are very interested usermode PAC bypass through DYLD. Synacktiv Slide
Another annoying mitigation is the JIT cage bypass, introduced with the A15 processor (iPhone 13 series). Previously, attackers could copy their shellcode into the JIT memory region. However, with JIT cage, instructions are restricted in JIT memory, preventing the execution of:
RET
BR/BLR/BL
SVC
MRS/MSR
PACDA/AUTDA
…
This aims to prevent attackers from calling arbitrary functions. The restricted information is configured in jitbox_cfg_set
in kernel (you can find it easily from KDK).
With usermode PAC bypass, JIT cage bypass is actually not mandatory, but without JIT cage bypass, to run additional payload from WebContent requires implementing some infrastructure, such as the [NSExpression exploit](https://googleprojectzero.blogspot.com/2023/10/an-analysis-of-an-in-the-wild-ios-safari-sandbox-escape.html).
Arbitrary code execution from WebContent has become more difficult, and public resources for user-mode PAC and JIT cage bypass are still rare.
We have some idea for this, and we’d like to refine it a bit more, and then cover it in a future post if we available.