背景

在 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

  1. 在用户空间,设置客户端的 Notification Port,随后在用户空间释放对这个 Notification Port 的引用。
  2. 在用户空间,配置解码请求,调用解码的异步方法。
  3. AppleJPEGDriverUserClient 在内部会再创建一个解码请求,并将 Notification Port 的地址设置到内部请求中。
  4. AppleJPEGDriverUserClient 向硬件抽象层提交解码请求。
  5. 在硬件抽象层处理解码请求的同时,在用户空间再次设置 Notification Port,这会造成 1 中设置的 Notification Port 被释放。
  6. 协处理器完成解码,产生中断,相关服务处理中断事件,通过 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

参考

  1. https://twitter.com/_simo36/status/1242932736219906048
  2. https://gist.github.com/0x36/3c9e77058eac7069616b72f0088d8b6d