CVE-2020-9768 分析:AppleJPEGDriverUserClient 中的 Port UaF
背景
在 iOS 13.4 发布后不久,@_simo36 在网上公开了 CVE-2020-9768 的漏洞概要及 PoC[1][2]
:
a POC trigger for my CVE-2020-9768, Apple description is not acurate, this is a kernel bug in AppleJPEGDriverUserClient
https://gist.github.com/0x36/3c9e77058eac7069616b72f0088d8b6d
AppleJPEGDriverUserClient 是一个可以在沙盒中(Container)中打开的客户端,它的功能是使用协处理器来加速 JPEG 编解码。我很久之前审计过这个客户端并没有发现什么问题,因此详细分析了下这个漏洞。
触发流程及 Root Cause
- 在用户空间,设置客户端的
Notification Port
,随后在用户空间释放对这个Notification Port
的引用。 - 在用户空间,配置解码请求,调用解码的异步方法。
AppleJPEGDriverUserClient
在内部会再创建一个解码请求,并将Notification Port
的地址设置到内部请求中。AppleJPEGDriverUserClient
向硬件抽象层提交解码请求。- 在硬件抽象层处理解码请求的同时,在用户空间再次设置
Notification Port
,这会造成1
中设置的Notification Port
被释放。 - 协处理器完成解码,产生中断,相关服务处理中断事件,通过
1
中设置的Notification Port
来通知用户空间。而这个Notification Port
已经被释放,从而造成 UaF。
如果想触发漏洞需要在内核空间执行步骤 4
时,在用户空间完成执行步骤 5
,因此涉及条件竞争。
实现分析
registerNotificationPort
的实现
__int64 AppleJPEGDriverUserClient::registerNotificationPort(AppleJPEGDriverUserClient *this, ipc_port *port, __int64 portType, int refCnt)
{
ipc_port *curPort = this->_notifyPort;
if ( curPort )
{
IOUserClient::releaseNotificationPort(curPort); // 释放之前的 Port
this->_notifyPort = 0LL;
}
this->_notifyPort = port;
this->_portRefCnt = refCnt;
return 0LL;
}
如上述代码,在设置新的 Port
时,会检查并且释放之前的 Port
。
startDecoder
的实现
__int64 __fastcall AppleJPEGDriverUserClient::startDecoder(AppleJPEGDriverUserClient *this, PRTS_AppleJPEGDriverIOStruct *stInput, PRTS_AppleJPEGDriverIOStruct *stOutput)
{
...
if ( stInput )
{
dstBufSize = stInput->dstBufSize;
if ( stInput->srcBufSize >= 0x100 )
isDstBufSizeValid = dstBufSize >= 7;
else
isDstBufSizeValid = 0;
if ( isDstBufSizeValid
&& stInput->srcWidth >= 4
&& stInput->srcHeight >= 2
&& stInput->dstWidth >= 4
&& stInput->dstHeight >= 2
&& !(stInput->unk2 >> 13)
&& stInput->subsampling < 0x2000 )
{
notifyPort = this->_notifyPort;
jpegDrv2 = jpegSrv;
internalRequest = PRTS_CreateInternalRequest(); // [A]: 创建内部请求
if ( internalRequest )
{
internalRequest->wakePort = notifyPort; // [B]: 设置 Port 指针
*&internalRequest->callback = *&stInput->asyncCallback;
internalRequest->args = *&stInput->asyncArgs;
HIDWORD(internalRequest->asyncRefCount) = 0;
...
cmdGate = jpegDrv2->_cmdGate;
funcPtr = OSMetaClassBase::_ptmf2ptf(jpegDrv2, PRTS_ProcessDecodeRequest, 0); // [C]: 将解码请求加入队列
v7 = cmdGate->_vptr->IOCommandGate::runAction(cmdGate, funcPtr, internalRequest, 0LL, 0LL, 0LL);
...
}
...
}
完成解码的事件处理
AppleJPEGDriver *__fastcall AppleJPEGDriver::PRTS_OnFinishedEvent(AppleJPEGDriver *this, PRTS_JPEGRequest *jpegRequest2, int a3, __int64 a4, char a5)
{
...
if ( jpegRequest2->callback )
{
userClient = OSMetaClassBase::safeMetaCast(jpegRequest2->userClient, &AppleJPEGDriverUserClient::gMetaClass);
if ( userClient && !(IOService::isInactive(userClient) & 1) )
{
*&jpegRequest2->asyncRef.wakePort = 0LL;
AppleJPEGDriverUserClient::setAsyncReference64(
&jpegRequest2->asyncRef,
jpegRequest2->wakePort, // [D]: 向 [B] 中设置的 Port 发通知
jpegRequest2->callback,
jpegRequest2->refcon);
v22 = AppleJPEGDriverUserClient::sendAsyncResult64(
&jpegRequest2->asyncRef,
jpegRequest2->result,
&jpegRequest2->args,
1u);
...
}
结合上面的代码片段,只要在 [B]
之后,[D]
之前释放 this->_notifyPort
就可以造成 UaF
。
参考
- https://twitter.com/_simo36/status/1242932736219906048
- https://gist.github.com/0x36/3c9e77058eac7069616b72f0088d8b6d