1. 程式人生 > Android開發 >iOS Jailbreak Principles 0x02 - codesign and amfid bypass

iOS Jailbreak Principles 0x02 - codesign and amfid bypass

系列文章

  1. iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox
  2. iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料
  3. iOS Jailbreak Principles - Undecimus 分析(三)通過 IOTrap 實現核心任意程式碼執行
  4. iOS Jailbreak Principles - Undecimus 分析(四)繞過 A12 的 PAC 實現 kexec
  5. iOS Jailbreak Principles 0x01 - rootfs remount r/w 原理

前言

之前的文章我們介紹了從核心漏洞到 tfp0,再到 rootfs 系統可讀寫。單純一個可讀寫的 rootfs 能做的事情還是非常有限的,為了能做更多事情我們往往要控制系統的 binary 或是分發自己的 binary 到系統。

為了能將自己或他人編寫的 binary 在 iOS 上跑起來,我們必須翻過程式碼簽名這座大山。iOS 僅僅包含了有限的 binary 和系統級 App,他們的簽名被 hardcode 在一份靜態的 TrustCache 中,對於我們自己部署的 binary,例如用於修改密碼的 passwd,以及用於 SSH 服務的 bash 和 dropbear,預設情況下是無法啟動的,會被 codesign 機制直接 kill 掉。

Codesign Chain

在 iOS 中,當執行一個 binary 時,系統會以責任鏈模式從多個角度檢查程式碼簽名,自 iOS 12 以後,整個程式碼簽名主要包含三個部分:

  1. TrustCache: 一份 binary cdhash 的快取,分為 static cache 和 dynamic cache 兩部分,當 binary 的 cdhash 命中時直接放行;
  2. CoreTrust: 核心基於 Apple 根證書對 binary 簽名的合法性校驗;
  3. AMFI:即 AppleMobileFileIntegrity,它會比對 binary 簽名中儲存的 cdhash 和 binary 實際 cdhash 是否相符。

TrustCache

TrustCache 本質上是 cdhash 的線性表,當 binary 執行時,系統首先計算出 binary 的 cdhash,隨後對 TrustCache 進行二分查詢,如果命中則直接放行。在 iOS 的 image 中包含了一份靜態的 TrustCache 用於加速系統 binary 的執行。

除去靜態 TrustCache 外,系統還會維護一份動態的 TrustCache,用於處理 Xcode 為裝置安裝除錯必須的 binary 的簽名問題[1]。這其實是我們 bypass codesign 的一個簡單方案

CoreTrust

它主要保證了 binary 簽名的合法性,即簽名所使用的證書是由 Apple 根證書所簽發的,這一機制使得非法簽名和無簽名的 binary 無法通過校驗。

AMFID

如果 binary 未命中又通過了 CoreTrust 檢查後(這裡不考慮 CoreTrust Cache),就會將訊息送達 amfid 進行真正的 codesign 檢查,這裡的核心是通過 MISValidateSignatureAndCopyInfo 方法對 binary 的實際 cdhash 和簽名中的 cdhash 進行驗證。

繞過思路

通過上面的討論我們知道整個 codesign 責任鏈主要的三環:

TrustCache (static + dynamic cache lookup) → 
CoreTrust (deny fake signs,must sign with certs from apple) → 
AMFI (cdhash check)
複製程式碼

TrustCache Poisoning

最簡單的方案就是篡改 dynamic TrustCache,我們首先通過 XREF 定位到 dynamic TrustCache 的全域性變數,它是一個連結串列,連結串列的每一個結點都儲存了一個或多個 binary 的 cdhash,且這些 cdhash 是以字典序升序排列的(用於支援二分查詢)。

我們只需要找到 dynamic cache 的全域性變數,為這個連結串列增加一個結點即可。這個在 rootlessJB 的 write-up[1] 以及各種開源 jailbreak 中有詳細的論述和程式碼,主要的入手點在 pmap_lookup_in_loaded_trust_caches,本文不展開。

CoreTrust Bypass

在 rootlessJB 的 write-up 中提到 binary 在 CoreTrust 的校驗也包含了一個基於 generation count 的快取,但為了構造出合法的快取可能需要模擬 XNU 中構造 cs_blob 的過程隨後再設定一個合法的 generation count。這種方式能直接跳過 AMFI 檢查。

AMFID Bypass

amfid 以 Mach Service 的形式提供對 codesign 的服務支援,既然是 C/S 架構,那麼一個簡單的方法就是偽造一個合法的響應,既然我們已經有了 task port,一個很直接的想法就是劫持 amfid 的相關邏輯,並返回簽名合法的訊息。

本文將主要介紹 AMFI Bypass 的分析過程以及實施手段。

How to Debug AMFID

在 iOS 11 以後,單純給 debugserver 簽上 platform-application,task_for_pid-allowcom.apple.system-task-ports 是依然無法 attach 到 system binary 的,因此預設情況下我們就無法除錯 amfid。

為了能除錯 system binary,我們必須在 spawn debugserver 時為它的 task 增加 TF_PLATFORM flag;其次為了斷點能正常工作,我們需要為被除錯的 proc 增加 CS_DEBUGGED flag:

static bool patch_proc(uint64_t proc) {
    printf("[*] patch proc 0x%llx",proc);
    uint64_t our_task = rk64(proc + 0x10);
    printf("[*] find our task at 0x%llx\n",our_task);
    
    uint32_t our_flags = rk32(our_task + 0x3B8);
    wk32(our_task + 0x3B8,our_flags | 0x00000400);
    printf("[+] give us TF_PLATFORM\n");

    uint32_t our_csflags = rk32(proc + 0x298);
    our_csflags = our_csflags | CS_DEBUGGED | CS_PLATFORM_BINARY | CS_INSTALLER | CS_GET_TASK_ALLOW;
    our_csflags = our_csflags & ~(CS_HARD | CS_KILL | CS_RESTRICT);
    wk32(proc + 0x298,our_csflags);
    printf("[+] give us CS_PLATFORM_BINARY | CS_INSTALLER | CS_GET_TASK_ALLOW\n");
    printf("[+] unrestrict our proc\n");
    return true;
}
複製程式碼

這裡需要用到 spawnAndPlatformize 的技術,這個技術包含在 QiLin ToolKit 中但沒有開源,缺乏對 iOS 13 的支援,我們可以轉而採用 jakeajames 開源在 rootlessJB 中的方法[3]:

int launchAsPlatform(char *binary,char *arg1,char *arg2,char *arg3,char *arg4,char *arg5,char *arg6,char**env) {
    pid_t pd;
    const char* args[] = {binary,arg1,arg2,arg3,arg4,arg5,arg6,NULL};
    
    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);
    posix_spawnattr_setflags(&attr,POSIX_SPAWN_START_SUSPENDED); //this flag will make the created process stay frozen until we send the CONT signal. This so we can platformize it before it launches.
    
    int rv = posix_spawn(&pd,binary,NULL,&attr,(char **)&args,env);
    
    platformize(pd);
    
    kill(pd,SIGCONT); //continue
    
    if (!rv) {
        int a;
        waitpid(pd,&a,0);
    }
    
    return rv;
}
複製程式碼

AMFID 分析

筆者這裡以 iOS 13.1.1 的 iPad Air 2 為樣本分析,我們可以從 iOS 裝置的 /usr/libexec/amfid 找到 amfid binary,將它進行反編譯後,我們從 main 入手分析:

void __fastcall __noreturn start(int a1,char **a2)
{
  char **argv; // x19
  int argc; // w20
  signed int v4; // w8
  signed int v5; // w22
  int hasDFlag; // w0
  __int64 v7; // x0
  void *v8; // x8
  int v9; // w1
  int v10; // w21
  int *v11; // x0
  char *v12; // x0
  const char *v13; // x1
  mach_port_t server_port; // [xsp+14h] [xbp-2Ch]
  struct dispatch_source_s *context; // [xsp+18h] [xbp-28h]
  dispatch_object_t v16; // 0:x0.8
  dispatch_object_t v17; // 0:x0.8

  argv = a2;
  argc = a1;
  v4 = 0;
  context = (struct dispatch_source_s *)-6148914691236517206LL;
  do
  {
    v5 = v4;
    hasDFlag = getopt(argc,argv,"d");
    v4 = 1;
  }
  while ( hasDFlag == 100 );
  if ( hasDFlag == -1 )
  {
    v7 = os_log_create("com.apple.MobileFileIntegrity","amfid");
    v8 = &_os_log_default;
    if ( v7 )
      v8 = (void *)v7;
    amfi_logger = v8;
    if ( v5 )
      v9 = 33;
    else
      v9 = 1;
    if ( v5 )
      v10 = 255;
    else
      v10 = 63;
    openlog("amfid",v9,24);
    setlogmask(v10);
    syslog(6,"starting");
    server_port = 0;
    if ( bootstrap_check_in(bootstrap_port,"com.apple.MobileFileIntegrity",&server_port) )
    {
      v11 = __error();
      v12 = strerror(*v11);
      syslog(3,"unable to checkin with launchd: %s",v12);
    }
    if ( server_port )
    {
      v16._do = dispatch_source_create(
                  (dispatch_source_type_t)&_dispatch_source_type_mach_recv,server_port,0LL,(dispatch_queue_t)&_dispatch_main_q);
      context = v16._do;
      if ( v16._do )
      {
        dispatch_set_context(v16,&context);
        dispatch_source_set_event_handler_f(context,(dispatch_function_t)amfi_server_port_event_handler);
        v17._do = context;
        dispatch_resume(v17);
        dispatch_main();
      }
      v13 = "could not create mig source";
    }
    else
    {
      v13 = "could not get mach port";
    }
    syslog(3,v13);
    exit(1);
  }
  fprintf(__stderrp,"unrecognized argument '%c'\n",(unsigned int)optopt);
  exit(1);
}
複製程式碼

這是一個 LaunchDaemon 的標準操作,通過 bootstrap_port 獲取自己的 service port 並監聽,重點看這一句:

dispatch_source_set_event_handler_f(context,(dispatch_function_t)amfi_server_port_event_handler);
複製程式碼

這裡我們得到了 server port 的 hander,我們跳轉到 handler 進行分析:

__int64 __fastcall amfi_server_port_event_handler(_QWORD *a1)
{
  _QWORD *v1; // x20
  __int64 v2; // x19
  __int64 v3; // x0

  v1 = a1;
  syslog(7,"%s: enter","mig_source_handler");
  v2 = os_transaction_create("amfid mig server");
  v3 = dispatch_mig_server(*v1,4184LL,amfi_mig_server_handler);
  if ( (_DWORD)v3 )
    syslog(3,"%s: dispatch_mig_server returned %d","mig_source_handler",v3);
  syslog(7,"%s: exit","mig_source_handler");
  return _os_release(v2);
}
複製程式碼

可以看到這裡包含了一個 mig server 的 handler,我們繼續向下分析:

signed __int64 __fastcall amfi_mig_server_handler(_DWORD *a1,__int64 a2)
{
  int v2; // w8
  int v3; // w8
  unsigned int some_index; // w8
  void (__cdecl *v5)(_DWORD *,__int64); // x8
  signed __int64 result; // x0

  *(_DWORD *)a2 = *a1 & 0x1F;
  v2 = a1[2];
  *(_DWORD *)(a2 + 4) = 36;
  *(_DWORD *)(a2 + 8) = v2;
  v3 = a1[5] + 100;
  *(_DWORD *)(a2 + 16) = 0;
  *(_DWORD *)(a2 + 20) = v3;
  *(_DWORD *)(a2 + 12) = 0;
  some_index = a1[5] - 1000;
  if ( some_index <= 4
    && (v5 = (void (__cdecl *)(_DWORD *,__int64))*(&off_100004090 + 5 * (signed int)some_index + 5)) != 0LL )
  {
    v5(a1,a2);
    result = 1LL;
  }
  else
  {
    result = 0LL;
    *(NDR_record_t *)(a2 + 24) = NDR_record;
    *(_DWORD *)(a2 + 32) = -303;
  }
  return result;
}
複製程式碼

這裡包含了一個 dispatch table,且 off_100004090 是跳轉表的頭部:

some_index = a1[5] - 1000;
if ( some_index <= 4
&& (v5 = (void (__cdecl *)(_DWORD *,__int64))*(&off_100004090 + 5 * (signed int)some_index + 5)) != 0LL )
{
v5(a1,a2);
result = 1LL;
}
複製程式碼

我們看一下 off_100004090 的內容:

__const:0000000100004090 off_100004090   DCQ mig_server_handler_inner_1
__const:0000000100004090                                         ; DATA XREF: mig_server_handler_inner_1+1C↑o
__const:0000000100004090                                         ; amfi_mig_server_handler+38↑o
// ...
__const:00000001000040B8                 DCQ mig_server_handler_inner_2
// ...
__const:00000001000040E0                 DCQ mig_server_handler_inner_3
複製程式碼

我們可以看到這裡包含了 3 個函式指標,基於不同的 index 會選擇不同的 handler 去處理 xpc message。

這裡我們可以採取動態除錯的方法去尋找實際被呼叫的 handler:

這裡我們可以看到實際用到的 handler 位於 0x00000001000032c8,即上面討論中的 mig_server_handler_inner_2

接下來順著 mig_server_handler_inner_2 分析,它是一個 wrapper,關鍵部分如下:

__n128 __fastcall mig_server_handler_inner_2(NDR_record_t *ndr,__int64 a2) {
    // ...
    ret = amfi_verify_codesign(
        a1 = ndr[1].int_rep,// via w0
        a2 = &ndr[5],// via x1 = binpath
        a3 = ndr[8].int_rep,// via x2
        a4 = ndr[9].int_rep,// via w3
        a5 = ndr[10].mig_vers,// via w4
        a6 = ndr[10].int_rep,// via w5
        a7 = arg1 + 0x24,// via x6
        a8 = arg1 + 0x28,// via x7,switch keypoint
        a9 = arg1 + 0x2c,// via x10
        a10 = arg1 + 0x30,// via x9
        a11 = arg1 + 0x34,// via x11
        a12 = arg1 + 0x38,// via x12
        a13 = arg1 + 0x44,// via x20,return cdhash
        a14 = &sp_cdhash_bytes,// via x8
        a15 = &ndr[13].int_rep   // via x8-prev
    );
// ...
}
複製程式碼

繼續跟進 amfi_verify_codesign,這裡給出關鍵程式碼:

uint64_t __fastcall amfi_verify_codesign(__int64 a1,__int64 a2,__int64 a3,char a4,__int64 a5,__int64 a6,_DWORD *a7,_DWORD *a8,_DWORD *a9,_DWORD *res_back_48,_DWORD *a11,_DWORD *a12,__int64 a13,__int64 cdhash_bytes,unsigned int *a15)
{
  _DWORD *res_back_40; // x19
  char v16; // w20
  __int64 bin_path; // x23
  uint64_t return_val; // x0
  uint64_t v19; // x25
  uint64_t binary_path; // x21
  __int64 cfdict; // x0
  uint64_t dict; // x22
  __int64 true_value; // x26
  uint64_t longnum_v; // x25
  __int64 error; // x0
  __int64 v26; // x25
  __int64 v27; // x0
  __int64 v28; // x24
  __int64 v29; // x23
  __int64 cdhash; // x23
  __int64 res_dict; // x25
  uint64_t singer_type; // x0
  __int64 cs_res_dict; // [xsp+50h] [xbp-170h]
  __int64 ndr_5_plus_reversed; // [xsp+58h] [xbp-168h]
  __int128 valuePtr; // [xsp+60h] [xbp-160h]
  __int128 v36; // [xsp+70h] [xbp-150h]
  __int128 v37; // [xsp+80h] [xbp-140h]
  __int128 v38; // [xsp+90h] [xbp-130h]
  __int128 v39; // [xsp+A0h] [xbp-120h]
  __int128 v40; // [xsp+B0h] [xbp-110h]
  __int128 v41; // [xsp+C0h] [xbp-100h]
  __int128 v42; // [xsp+D0h] [xbp-F0h]
  __int128 v43; // [xsp+E0h] [xbp-E0h]
  __int128 v44; // [xsp+F0h] [xbp-D0h]
  __int128 v45; // [xsp+100h] [xbp-C0h]
  __int128 v46; // [xsp+110h] [xbp-B0h]
  __int128 v47; // [xsp+120h] [xbp-A0h]
  __int128 v48; // [xsp+130h] [xbp-90h]
  __int128 v49; // [xsp+140h] [xbp-80h]
  __int128 v50; // [xsp+150h] [xbp-70h]
  __int64 v51; // [xsp+168h] [xbp-58h]

  res_back_40 = a8;
  v16 = a4;
  bin_path = a2;
  ndr_5_plus_reversed = a3;
  *a7 = 0;
  *a8 = 0;
  *res_back_48 = 0;
  *a11 = 0;
  *a12 = 0;
  *a9 = 0;
  *(_OWORD *)cdhash_bytes = 0uLL;               // x24 = cdhash_bytes out
  *(_DWORD *)(cdhash_bytes + 16) = 0;
  if ( !memcmp(a15,&unk_100003BB8,0x20uLL) )
  {
    v19 = kCFAllocatorDefault;
    t
    if ( return_val )
    {
      binary_path = return_val;
      cfdict = CFDictionaryCreateMutable(v19,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks);
      if ( cfdict )
      {
        dict = cfdict;
        true_value = kCFBooleanTrue;
        CFDictionarySetValue(cfdict,kMISValidationOptionValidateSignatureOnly,kCFBooleanTrue);
        CFDictionarySetValue(dict,kMISValidationOptionRespectUppTrustAndAuthorization,true_value);
        longnum_v = CFNumberCreate(v19,0xBuLL,&ndr_5_plus_reversed);
        CFDictionarySetValue(dict,kMISValidationOptionUniversalFileOffset,longnum_v);
        CFRelease(longnum_v);
        cs_res_dict = 0LL;
        error = MISValidateSignatureAndCopyInfo(binary_path,dict,(uint64_t)&cs_res_dict);
        if ( (_DWORD)error )
        {
          // error
        }
        else if ( cs_res_dict
               && (v29 = CFGetTypeID(),v29 == CFDictionaryGetTypeID())
               && (cdhash = CFDictionaryGetValue(cs_res_dict,kMISValidationInfoCdHash)) != 0
               && (res_dict = CFGetTypeID(),res_dict == CFDataGetTypeID()) )
        {
          CFDataGetBytes(cdhash,20LL,cdhash_bytes);
          singer_type = CFDictionaryGetValue(cs_res_dict,kMISValidationInfoSignerType);
          if ( singer_type )
          {
            *(_QWORD *)&valuePtr = 0LL;
            if ( CFNumberGetValue(singer_type,0xEuLL,&valuePtr) )
            {
              if ( (_QWORD)valuePtr == 5LL )
                *res_back_48 = 5;
            }
            else if ( (unsigned int)os_log_type_enabled(amfi_logger,16LL) )
            {
              amfi_log_error_some(binary_path,&cs_res_dict);
            }
          }
          *res_back_40 = 1;
        }
        else
        {
          if ( (unsigned int)os_log_type_enabled(amfi_logger,17LL) )
            amfi_log_error_some2(binary_path,&cs_res_dict);
            *res_back_40 = 0;
        }
        if ( cs_res_dict )
          CFRelease(cs_res_dict);
        if ( v16 & 1 )
          *res_back_40 = 0;
        CFRelease(dict);
      }
      return_val = CFRelease(binary_path);
    }
  }
  else
  {
    // error
  }
  return return_val;
}
複製程式碼

這裡的幾個關鍵部分如下:

  1. 通過 return_val = CFStringCreateWithFileSystemRepresentation(kCFAllocatorDefault,bin_path); 我們可以知道 a2 是 binary path,它通過 ndr[5] 傳入,被儲存在 x23 中;
  2. 簽名校驗的關鍵邏輯在 libmis.dylib 的 MISValidateSignatureAndCopyInfo 中,函式必須返回 0 和合法的 dict 才能繼續後面的校驗;
  3. 通過 CFDataGetBytes(cdhash,0LL,20LL,cdhash_bytes); 完成了 binary cdhash 的拷貝,其中 cdhash_bytes 的地址儲存在 x24 中;
  4. res_back_40 在出錯時均寫了 0,成功時寫了 1,因此他應該代表校驗的結果,它通過 a8 傳入,通過分析 Caller 可知 a8 的地址被儲存在 x19 中。

基於上面的分析,我們的主要任務是偽造出 res_back_40,但經過實驗發現單純偽造 result 的 true/false 是不夠的,我們還需要將 binary 實際的 cdhash 寫入到 x24 對應的地址中才能完美的模擬 amfi_verify_codesign 從而通過簽名校驗。

AMFI 繞過

有了上面的分析我們知道,關鍵是要在 amfi_verify_codesign 中偽造三個東西:

  1. 計算 binary 的真實 cdhash 並寫到 x24 對應的 Caller Stack 地址,這個可以通過 x23 先拿到 binary path,呼叫 MIS 方法完成計算後寫回;
  2. 劫持 MISValidateSignatureAndCopyInfo 使其返回 0;
  3. res_back_40 置為 1。

這些在 jakeajames 的 jelbrekLib 中已經有非常成熟的開源方案[4],核心思路是獲取 amfid 的 task port,為它設定一個 exception port,並將其 MISValidateSignatureAndCopyInfo 符號的地址寫成非法值,當 AMFI 執行簽名校驗時,我們會收到 mach exception message,隨後執行上述繞過操作,直接跳轉到 amfi_verify_codesign 的 Epilogue 即可,這裡給出幾份程式碼實現的地址:

  1. github.com/jakeajames/…
  2. github.com/coolstar/Ch…

總結

本文先簡要分析了 iOS 12 以後的 codesign 機制,隨後從 amfid 入手分析了其繞過方案的原理和實施過程。

參考資料

  1. jakeajames: rootlessJB write-up
  2. Jonathan Levin: Make Debugging Great Again
  3. jakeajames: rootlessJB_EL - launchAsPlatform
  4. jakeajames: jelbrekLib - amfid.m
  5. CoolStar: Chimera13 - amfidtakeover.swift