看我如何繞過 iPhone XS 中指標驗證機制 (上)
在這篇文章中,我將研究蘋果在 iPhone XS 中使用的 A12 晶片上實現的指標驗證技術,重點是蘋果在ARM標準上的改進。然後,我演示了一種偽造核心指標的 PAC 簽名的方法,藉助於JOP(Jump-Oriented Programming),這足以在核心中執行任意程式碼。遺憾的是,這項技術都在 12.1.3 中基本上被修復。事實上,針對這個漏洞的修復最初是出現在 16D5032a beta 版中,而當時我的研究已經在進行中了。
ARMv8.3-A 指標驗證
在ARMv8.3-A中,引入最大的防護機制是指標驗證功能。引入指標驗證後,當前大部的 POC 指令碼都會失效。這個功能主要是利用指標的高位來儲存 指標驗證碼 (PAC, Pointer Authentication Code)。它本質上就是一個指標值和一段附加資訊的密碼簽名。在ARMv8.3-A中引入了一些特殊的指令,用於對指標 PAC 的新增,驗證以及恢復。這使系統能夠在密碼層面確保某些指標不被攻擊者篡改,從而極大地提高應用程式的安全性。
指標驗證的基本想法是,儘管指標的長度是64位元,大部分的系統的虛擬地址空間是遠遠比這個小的,這樣會在指標中留下一些沒有使用的位,而這些位可以用來儲存額外的資訊。在指標驗證中,這些空閒位被用來儲存一個比較短的驗證碼,這個驗證碼就是指的對原始的 64 位指標和 64 位上下文的簽名。我們可以在將指標寫入記憶體之前向每個想要保護的指標中插入 PAC,並在使用它之前驗證它的完整性。攻擊者想要修改受保護指標,必須找到或猜解正確的 PAC 才能控制程式流。
在系統的實現中,系統可以定義自己的演算法來實現 PAC 的簽名與驗證,但是白皮書中建議使用 QARMA 分組密碼。白皮書稱,QARMA 是專門為指標驗證設計的“一種輕量級可調整的塊密碼家族”,它能夠接受一個128位金鑰、一個64位明文值(指標)和一個64位tweak(上下文)作為輸入,並生成一個64位的密文。最後,將密文截斷後成為 PAC, PAC 被插入到指標未使用的擴充套件位中。
已經有許多文章來描述指標驗證了,所以我在這裡也只需要粗略的描述一些原理。
感興趣的讀者可以參考 Qualcomm的白皮書 、Mark Rutland在2017年Linux安全峰會上的 幻燈片 、Jonathan Corbet的 LWN文章 以及 ARM A64指令集架構 以獲得更多細節。
在PAC機制中,系統提供了 5 個 128 位元的金鑰。其中兩個金鑰( APIAKey 和 APIBKey )用於指令指標。另外兩個( APDAKey 和 APDBKey )用與資料指標。最後還有一個金鑰( APGAKey )是一個特殊的通用金鑰,用於通過 PACGA 指令對較大的資料塊進行簽名。提供多個金鑰能夠使系統對指標替換攻擊具有一些基本的保護能力。
這些金鑰的值會寫入了一個特殊的系統暫存器中。這個暫存器在 EL0 中是無法訪問的,意味著使用者空間的程序不能讀取或者修改它們。然而,在硬體層面沒有提供任何其他的金鑰管理功能:由每個異常級別(EL, Exception Level)執行的程式碼來管理較低異常級別的金鑰。
為了處理 PAC, ARMv8.3-A 新引入了三類指令:
- PAC* 類指令可以向指標中生成和插入 PAC。 比如,PACIA X8,X9 可以在暫存器X8中以 X9 為上下文,APIAKey 為金鑰,為指標計算 PAC,並且將結果寫回到 X8 中。同樣的 PACIZA 跟 PACIA 類似,不過上下文固定為0。
- AUT* 類指令可以驗證一個指標的 PAC。 如果PAC是合法的,將會還原原始的指標。否則,將會在指標的擴充套件位中將會被寫入錯誤碼,在指標被簡接引用時,會觸發錯誤。比如,AUTIA X8,X9 可以以 X9 為上下文,驗證 X8 暫存器中的指標。當驗證成功時會將指標寫回 X8,失敗時則寫回一個錯誤碼。
- XPAC* 類指令可以移除一個指標的 PAC 並且在不驗證指標有效性的前提下恢復指標的原始值。
為了將指標認證與現有的操作結合起來,除了這些一般的指標認證指令外,還引入了一些特殊的變體指令:
- BLRA* 類指令實現了一個 “authenticate-and-branch” 的操作:如果指標是合法的,則會直接branch到該地址。比如,BLRAA X8,X9 可以用指令金鑰A(APIAKey),並且以 X9 作為上下文驗證 X8 指標的合法性,如果合法則會 branch 到結果地址上。
- LDRA* 類指令實現了一個 “authenticate-and-load” 的操作:如果指標是合法的,則會直接load到該地址。比如,LDRAA X8,X9 可以用資料金鑰A(APDAKey),驗證指標 X9, 上下文為 0,如果合法,則會將結果load到X8中。
- RETA* 類指令實現了一個 “authenticate-and-return” 的操作:如果LR暫存器合法,則會執行RET指令。比如,RETAB 將會利用指令金鑰B(APIBKey)驗證 LR 的合法性,並且返回。
一個缺陷:簽名元件
在我們開始分析 PAC 之前,我需要首先提一下一個已知的缺陷:如果一個攻擊者有讀寫許可權,可以呼叫系統的簽名元件(signing gadgets),那麼 PAC 是可以被繞過的。簽名元件指的是一組可用於對任意指標簽名的指令序列。如果攻擊者可以觸發一個函式的執行,簽名元件就從記憶體中讀取指標、新增PAC並將其寫回,那麼攻擊者就可以利用這個過程來偽造任意指標的PAC。
面對核心層攻擊者的設計缺陷
就像Qualcomm白皮書中所說的那樣,ARMv8.3 中指標驗證設計的目的是,在攻擊者具有任意記憶體讀寫許可權的情況下來為系統提供一定程度的保護。然而我們的威脅模型中,核心攻擊者已經有了讀寫許可權,並且希望通過在核心指標上偽造 PAC 來執行任意程式碼。在面對核心層次攻擊者時,我指出了設計中的三個潛在弱點:
攻擊者從記憶體中讀取PAC金鑰、在使用者空間中對核心指標進行簽名,以及使用 B 類金鑰對 A 類金鑰的指標進行簽名(反之亦然)。接下來我們將依次討論每個問題。
從核心層的記憶體中讀取PAC金鑰
首先,我們考慮一下最主要的攻擊形式:攻擊者從核心空間的記憶體中讀取PAC金鑰,然後計算任意核心指標的PAC。以下是白皮書中關於這類攻擊者部分的摘錄:
指標驗證的目的是抵抗記憶體洩露攻擊。PAC 是使用強加密演算法保護的,因此從記憶體中讀取加密後的指標都不比偽造指標簡單。 PAC 金鑰儲存在暫存器中,這些暫存器不能從使用者態(EL0)直接訪問。因此,普通的記憶體洩露漏洞是不能用於提取 PAC 金鑰的。
雖然這個描述是正確的,但是它僅僅適用於攻擊使用者空間程式,而不是攻擊核心本身。最近的 iOS 裝置似乎沒有執行 hypervisor (EL2)或 secure monitor(EL3),這意味著在系統核心(EL1)必須管理自己的 PAC 金鑰。由於儲存金鑰的系統暫存器在核心休眠時將被清除,PAC金鑰不得不儲存在核心的記憶體中。因此,具有核心空間記憶體訪問許可權的攻擊者可能會讀取金鑰,並使用金鑰來算出任意指標的 PAC。
當然,這種攻擊方法是假定了我們能夠知道生成PAC是使用了什麼演算法,我們能夠在使用者態中實現它。
但是,按照蘋果的一貫作風,他們有很大的可能會實現一個自己的演算法來替代 QARMA,而不是直接使用 QARMA。
如果是這樣的話,那麼即使攻擊者知道了 PAC 金鑰,還是不足以偽造指標的 PAC: 要麼我們對蘋果的晶片進行逆向來還原簽名演算法,要麼我們必須找到一種方法來利用現有的機制來為我們的偽造指標簽名。
跨 EL 層的PAC偽造
一種可能的方法是通過在使用者空間中執行相應的PAC*指令來偽造核心指標的PAC。雖然這聽起來不太可能,但有幾個原因可以說明這是可行的。
蘋果可能已經決定對EL0和EL1使用相同的PAC金鑰,在這種情況下,我們可以直接從使用者空間對核心空間的指標執行 PACIA 指令,來偽造一個核心的指標的 PAC。在文件中可以看出來,描述 PAC* 指令的虛擬碼並不區分該指令是在EL0還是在EL1上執行的,因此核心態與使用者態應該具有相同的呼叫方式。
下面就是 AddPACIA() 函式的虛擬碼,描述了類似 PACIA 指令的實現:
// AddPACIA() // ========== // Returns a 64-bit value containing X, but replacing the pointer // authentication code field bits with a pointer authentication code, where the // pointer authentication code is derived using a cryptographic algorithm as a // combination of X, Y, and the APIAKey_EL1. bits(64) AddPACIA(bits(64) X, bits(64) Y) boolean TrapEL2; boolean TrapEL3; bits(1)Enable; bits(128) APIAKey_EL1; APIAKey_EL1 = APIAKeyHi_EL1<63:0>:APIAKeyLo_EL1<63:0>; case PSTATE.EL of when EL0 boolean IsEL1Regime = S1TranslationRegime() == EL1; Enable = if IsEL1Regime then SCTLR_EL1.EnIA else SCTLR_EL2.EnIA; TrapEL2 = (EL2Enabled() && HCR_EL2.API == '0' && (HCR_EL2.TGE == '0' || HCR_EL2.E2H == '0')); TrapEL3 = HaveEL(EL3) && SCR_EL3.API == '0'; when EL1 Enable = SCTLR_EL1.EnIA; TrapEL2 = EL2Enabled() && HCR_EL2.API == '0'; TrapEL3 = HaveEL(EL3) && SCR_EL3.API == '0'; ... if Enable == '0' then return X; elsif TrapEL2 then TrapPACUse(EL2); elsif TrapEL3 then TrapPACUse(EL3); else return AddPAC(X, Y, APIAKey_EL1, FALSE);
下面是 AddPAC() 函式的虛擬碼:
/ AddPAC() // ======== // Calculates the pointer authentication code for a 64-bit quantity and then // inserts that into pointer authentication code field of that 64-bit quantity. bits(64) AddPAC(bits(64) ptr, bits(64) modifier, bits(128) K, boolean data) bits(64) PAC; bits(64) result; bits(64) ext_ptr; bits(64) extfield; bit selbit; boolean tbi = CalculateTBI(ptr, data); integer top_bit = if tbi then 55 else 63; // If tagged pointers are in use for a regime with two TTBRs, use bit<55> of // the pointer to select between upper and lower ranges, and preserve this. // This handles the awkward case where there is apparently no correct // choice between the upper and lower address range - ie an addr of // 1xxxxxxx0... with TBI0=0 and TBI1=1 and 0xxxxxxx1 with TBI1=0 and // TBI0=1: if PtrHasUpperAndLowerAddRanges() then ... else selbit = if tbi then ptr<55> else ptr<63>; integer bottom_PAC_bit = CalculateBottomPACBit(selbit); // The pointer authentication code field takes all the available bits in // between extfield = Replicate(selbit, 64); // Compute the pointer authentication code for a ptr with good extension bits if tbi then ext_ptr = ptr<63:56>:extfield<(56-bottom_PAC_bit)-1:0>:ptr<bottom_PAC_bit-1:0>; else ext_ptr = extfield<(64-bottom_PAC_bit)-1:0>:ptr<bottom_PAC_bit-1:0>; PAC = ComputePAC(ext_ptr, modifier, K<127:64>, K<63:0>); // Check if the ptr has good extension bits and corrupt the pointer // authentication code if not; if !IsZero(ptr<top_bit:bottom_PAC_bit>) && !IsOnes(ptr<top_bit:bottom_PAC_bit>) then PAC<top_bit-1> = NOT(PAC<top_bit-1>); // Preserve the determination between upper and lower address at bit<55> // and insert PAC if tbi then result = ptr<63:56>:selbit:PAC<54:bottom_PAC_bit>:ptr<bottom_PAC_bit-1:0>; else result = PAC<63:56>:selbit:PAC<54:bottom_PAC_bit>:ptr<bottom_PAC_bit-1:0>; return result;
由此可以看出,在執行 PACIA 時,無論執行在 EL0 和 EL1,在操作上並沒有太大的區別。這意味著如果蘋果對兩個不同級別的指標使用了相同的 PAC 金鑰,那麼我們就可以在使用者空間中執行 PACIA 函式來為核心空間中指標生成 PAC。
當然,蘋果似乎不太可能在它們的系統中留下如此明顯的漏洞。但即使如此,由於 EL0 與 EL1 的對稱性,我們仍然可以在使用者空間中生成核心空間指標的簽名,只不過需要函式需要的金鑰替換為核心空間的金鑰即可。在蘋果使用新演算法替代QARMA的情況下,這個方法會非常有用,因為我們可以重用現有的簽名演算法,而不必對其進行反向工程。
交叉金鑰偽造 PAC
除了 EL0 與 EL1 之間演算法的對稱性,我們還可以利用它們金鑰之間的對稱性來生成偽造的 PAC : PACIA, PACIB, PACDA, and PACDB 這些金鑰都可以看作為相同演算法的不同引數。因此,如果我們可以用一個 PAC 金鑰來替換另一個 PAC 金鑰(A 與 B交換),那麼我們就可以將一個金鑰簽名的過程替換成了另一個金鑰的簽名過程。
雖然這種方案並不是特別強大,但是這個方法在某些情況下十分有用。比如,如果 PAC 演算法是未知的,並且有一些機制能阻止我們將使用者空間 PAC 金鑰設定為與核心 PAC 金鑰相等,我們將不能進行跨 EL 偽造,但是我們卻可以利用這種方案。雖然我們還是需要依賴 PAC 簽名工具,但是這種技術將使我們擺脫簽名工具使用固定金鑰進行簽名的限制,可能會使我們的簽名工具多樣化。
小結
在本文中,我們介紹了指標驗證的原理,分析了指標驗證的一些缺陷,並在理論上提出了一些可能的方案來繞過以及偽造指標驗證機制。在下一篇文章中,我們會根據真實的場景,利用幾個已知的漏洞來繞過 A12 晶片中的指標驗證,並偽造指標驗證碼。