讓你的應(yīng)用遠離越獄:iOS 14 App Attest 防護功能
當(dāng)越獄在 iOS 設(shè)備第一次流行起來時,iOS 開發(fā)人員會嘗試各種方法來保護自己的應(yīng)用程序,以讓應(yīng)用免受盜版等不確定因素的困擾。有許多方法可以做到這一點,包括檢查 Cydia 是否存在、檢測應(yīng)用程序是否可讀取自身沙箱之外的文件、在檢測到調(diào)試器時讓應(yīng)用程序崩潰等等。
然而,事實證明這些防御措施并不是那么有效。如果攻擊者可以直接訪問物理設(shè)備,那么這些措施就不再有效。對于高手來說,他們可以讓設(shè)備看上去并沒有越獄以有效地繞過這些措施,過去可以,現(xiàn)在也可以。同時對于一些越獄用戶來說,他們可能并不是想要干壞事,而僅僅是想要一些酷炫的功能,比如說可定制的主屏幕。
隨著近來越獄可能再度流行,Apple 給出了一套自己的解決方案。在 iOS 14 中,新的 App Attest API 為應(yīng)用提供了一種對服務(wù)器請求進行簽名的方法,以嘗試向服務(wù)器證明這些請求來自應(yīng)用程序的合法版本。
需要了解的是,App Attest 不會告訴服務(wù)器“這個設(shè)備是越獄的么?”,因為這種方案被一次次證明是不可行的。相反,其目標(biāo)是保護服務(wù)器請求,以讓攻擊者更難創(chuàng)建非法的應(yīng)用版本來解鎖高級功能或植入作弊功能。再次強調(diào)的是:由于攻擊者可以物理訪問設(shè)備,因此在這種情況下,沒有任何辦法可以完全保護你的應(yīng)用。
由于無法信任應(yīng)用可以自我保護,因此 App Attest 要求在應(yīng)用的后端采取必要的工作來實施這個安全策略。由于這是 Swift 相關(guān)的內(nèi)容,所以這里不介紹后端應(yīng)該如何處理,只是會順帶提及。
生成一對密鑰以簽署請求
App Attest 依賴于使用非對稱公鑰/密鑰對來工作。最終目的是讓應(yīng)用程序使用密鑰對服務(wù)器請求進行簽名,然后將數(shù)據(jù)發(fā)送到后端,在后端用公鑰來確認請求的合法性。如果攻擊者攔截了請求,他并沒有辦法更改內(nèi)容,這樣就不會影響后端的驗證。
要生成密鑰對,可以導(dǎo)入 DeviceCheck 框架,并調(diào)用 DCAppAttestService 單例對象的 generateKey 方法:
- import DeviceCheck
- let service = DCAppAttestService.shared
- service.generateKey { (keyIdentifier, error) in
- guard error == nil else {
- return
- }
- }
App Attest 生成的密鑰對會安全地存儲在設(shè)備的 Security Enclave 中。由于無法直接訪問,所以這個方法返回的是一個 keyIdentifier 屬性,在需要時可以用來找到對應(yīng)的密鑰。我們需要存儲它,以便后續(xù)用來驗證應(yīng)用程序的請求。
值得一提的是,并非所有類型的設(shè)備都支持App Attest,如果查看了 Apple 的文檔,會發(fā)現(xiàn)我們需要先檢查是否支持,并要求服務(wù)器做降級處理以應(yīng)用例外的情況:
- if service.isSupported { ... }
但是不要這么做!就像之前所說的,攻擊者可以可以輕松地偽裝成設(shè)備不支持這一操作。Apple 也沒有相應(yīng)的應(yīng)對措施,這個檢查的原因更多的是因為有些 Macbook 沒有支持它的芯片。根據(jù) Guilherme Rambo 的調(diào)查,大部分 iOS 設(shè)備都支持這一功能,所以對應(yīng) iOS 應(yīng)用來說,不需要這個兼容性檢測。
將公鑰發(fā)送到后端
為了對請求進行簽名,需要為后端提供一種校驗簽名的方法。我們需要為后端提供上述生成的公鑰的訪問權(quán)限,來完成校驗。但是我們不能簡單地創(chuàng)建一個請求來發(fā)送公鑰,因為攻擊者很容易攔截請求并發(fā)送自己的公鑰,這樣他們可以完全控制應(yīng)用發(fā)送到后端的內(nèi)容。
這個問題的解決方法是讓 Apple 來證明我們發(fā)送的密鑰是來自應(yīng)用的合法版本??梢哉{(diào)用 attestKey 方法來完成,該方法接收密鑰的標(biāo)識符作為參數(shù):
- service.attestKey(keyIdentifier, clientDataHash: hash) { attestation, error in
- guard error == nil else { return }
- let attestationString = attestation?.base64EncodedString()
- // Send the attestation to the server. It now has access to the public key!
- // If it fails, throw the identifier away and start over.
- }
這個方法會訪問遠程 Apple 服務(wù)器,并返回一個 "attestation" 對象,這個對象不僅包含了公鑰,而且還包含有關(guān)應(yīng)用程序的大量信息,以表明這是經(jīng)過 Apple 認證的合法的公鑰??蛻舳耸盏竭@個對象后,必須將其完整發(fā)送到后端,后端需要執(zhí)行多步驗證,以確認未被篡改。如果驗證了 "attestation" 對象是合法的,后端便可以從中安全地提取應(yīng)用的公鑰。
目前尚不清楚 Apple 是否嘗試在此過程中檢查用戶的設(shè)備是否越獄。文檔并沒有提到這種情況,不過他們也指出 App Attest 不能確切地設(shè)備是否越獄,這至少說明他們嘗試過??梢钥隙ǖ卣f,并沒有辦法指出設(shè)備是否越獄,而且 attest 這個詞只表示請求未被攔截或篡改。
attestation 請求的附加 clientDataHash 參數(shù)與校驗的過程本身無關(guān),但對安全性卻至關(guān)重要。實際上,這個請求的很容易做重放攻擊,攻擊者可以攔截驗證請求并竊取從 Apple 發(fā)送的 "attestation" 對象,以便后續(xù)可以在應(yīng)用程序的非法版本中 “重放” 相同的驗證請求來欺騙服務(wù)器。
解決這個問題的方法是簡單粗暴地不允許驗證請求被隨意地執(zhí)行??蛻舳丝梢蕴峁┮粋€一次性使用令牌(或會話ID),服務(wù)器希望該令牌與請求一起使用以確保其有效性。如果兩次使用相同的令牌,則請求將失敗。這就是 clientDataHash 的目的:通過向驗證請求提供令牌的哈希版本,Apple 會將其嵌入到最終對象中,并為您的服務(wù)器提供一種提取它的方式。有了這個,對于攻擊者來說,僅通過攔截請求就很難創(chuàng)建您應(yīng)用程序的非法版本。
- let challenge = getSessionId().data(using: .utf8)!
- let hash = Data(SHA256.hash(data: challenge))
- service.attestKey(keyIdentifier, clientDataHash: hash) { ... }
如前所述,Apple 并不建議你重用密鑰,而應(yīng)該對設(shè)備中的每個用戶帳戶執(zhí)行整個過程。
由于這個請求依賴于遠程 Apple 服務(wù)器,因此可能會失敗。如果錯誤是服務(wù)器不可用,Apple 表示你可以重試,但是如果其他原因,則應(yīng)丟棄密鑰標(biāo)識符并重新開始這一流程。例如,當(dāng)用戶重新安裝您的應(yīng)用程序時,可能會發(fā)生這種情況:你生成的密鑰在正常的應(yīng)用程序更新中仍然有效,但是在重新安裝應(yīng)用程序,設(shè)備遷移或從備份還原設(shè)備后仍然會發(fā)生錯誤。對于這些情況,您的應(yīng)用需要能夠重新執(zhí)行密鑰生成過程。
從服務(wù)器方面來說,還值得一提的是,"attestation" 對象還包含一張回執(zhí),你的服務(wù)器可以使用該回執(zhí)來向 Apple 請求欺詐評估指標(biāo)。這使你可以檢查生成的密鑰的數(shù)量以及與它們關(guān)聯(lián)的設(shè)備,以檢測可能的欺詐情況。蘋果公司特別提到了攻擊的可能性,即用戶可能使用一個設(shè)備向越獄設(shè)備提供有效的斷言,這種欺詐評估可以通過定位具有異常高數(shù)量的斷言請求的用戶來檢測到。
加密請求
在驗證了密鑰的有效性之后,后端將可以訪問公鑰。從現(xiàn)在開始,每次處理敏感內(nèi)容時,都可以安全地對請求進行簽名。用于此目的的 generateAssertion 方法的工作原理與密鑰的驗證非常相似,只是這次需要要驗證請求本身:
- let challenge = getSessionId().data(using: .utf8)!
- let requestJSON = "{ 'requestedPremiumLevel': 300, 'sessionId': '\(challenge)' }".data(using: .utf8)!
- let hash = Data(SHA256.hash(data: challenge))
- service.generateAssertion(keyIdentifier, clientDataHash: hash) { assertion, error in
- guard error == nil else { return }
- let assertionString = assertion?.base64EncodedString()
- // Send the signed assertion to your server.
- // The server will validate it, grab your request and process it.
- }
與之前一樣,后端必須支持使用一次性令牌來防止重放攻擊。這次,由于請求本身就是我們的 clientDataHash,因此我們將令牌添加到 JSON 中。對于給定鍵可以進行的斷言數(shù)量沒有限制。但是,盡管如此,通常仍應(yīng)保留它們,以在應(yīng)用程序發(fā)出請求保護敏感信息,例如下載內(nèi)容。
在這種情況下,額外保護來源于請求被散列并且只能使用一次。由于整個請求都是由私鑰簽名的,因此攻擊者無法簡單地攔截請求并利用它們來制作自己的請求。他們必須弄清楚你請求的參數(shù)來自何處,并手動嘗試對其進行簽名,這比簡單附加代理要更多的技術(shù)。如開頭所述,要破解這種保護并不是沒有可能,只是需要更加努力。
測試及實施
App Attest 服務(wù)記錄了你無法重置的標(biāo)記。為防止這種情況,非生產(chǎn)環(huán)境中的應(yīng)用程序?qū)⑹褂蒙澈邪姹?。如果你想在生產(chǎn)環(huán)境中進行測試,則應(yīng)將 com.apple.developer.devicecheck.appattest-environment 授權(quán)添加到你的應(yīng)用中,并將其值設(shè)置為 production。
如果你的用戶群很大,Apple 建議你逐步啟用此功能,因為對 attestKey 的請求受網(wǎng)速限制。
結(jié)論
通過在客戶端和后端中實現(xiàn)此功能,攻擊者更難創(chuàng)建應(yīng)用程序的非法版本。但是,請注意這并不意味著不可能!如前所述,你無法確定用戶是否擁有越獄設(shè)備,也無法確定阻止其攻擊你的應(yīng)用的方法。與大多數(shù)安全措施一樣,App Attest 的目的是使此過程足夠困難,以使只有一個非常熟練和專業(yè)的攻擊者才能找到闖你您的應(yīng)用程序的途徑-而這種牛人很少。