今年的 WWDC 剛過去,蘋果生態當中的幾大作業系統都宣布了全新的測試版本。比如 macOS 升級到了 12.0,命名為 Monterey。
有眼尖的網友在新版文檔當中看到了這個新加入的 API:
int xpc_connection_set_peer_code_signing_requirement( xpc_connection_t connection, const char *requirement);這個 API 屬於 XPC 框架。
XPC
XPC 是 macOS 和 iOS 當中一種基於 Mach 消息的跨進程通信機制,其 API 可以很輕鬆地將計算任務拆分到多個進程中完成。特別是系統會在消息發送的時候自動啟動對應的 service,開發者只需關心 connection 的生命周期,而不是具體的進程。
進程隔離可以提升應用的安全性和穩定性。例如在處理格式解析的時候,損壞或者惡意構造的文件可能造成進程崩潰。有了 XPC,開發者可以將這類容易出問題的運算任務放進獨立進程中實現,並使用嚴格的沙箱配置。
一方面 XPC 進程錯誤不會讓主程序崩潰,另一方面沙箱也會限制漏洞的利用。例如 Safari 瀏覽器和 WKWebView 就利用了這種機制,採用獨立進程渲染 Web 內容。
XPC 的目的是簡化多進程權限隔離。XPC 服務不一定會採用更嚴格的沙箱,反之,還有一種特權 XPC 服務的應用。
在 macOS 系統中想要臨時以管理員權限執行操作,系統會要求輸入密碼或者使用 TouchID 驗證身份。實際上後臺程序臨時的權限提升需求很常見,如果每一次都要求認證,會對用戶體驗造成一定程度的損害。
macOS 將 AuthorizationExecuteWithPrivileges 函數標記為過時,並推薦使用 SMJobBless 替代。但這兩個函數的行為並不等價,甚至可以說是大相逕庭。AuthorizationExecuteWithPrivileges 類似 sudo,可以創建更高權限的子進程;SMJobBless 則會向系統中安裝一個 PrivilegedHelper 並註冊為 Mach Service。
這樣一來不是直接 sudo 執行命令,而是僅在第一次安裝時需要驗證管理員身份,之後後臺的 XPC Helper 以 root 權限運行,界面等較低權限的程序通過 XPC 暴露的接口遠程調用 XPC Helper 裡的代碼。
這個機制對第三方開發者開放,而另一方面 mac 系統本身有很多內置的 XPC 服務。
這樣便產生了跨安全邊界的通信,需要考慮惡意輸入的問題。做安全設計需要具備一個思維就是,不要輕易信任數據輸入。
我們來看一些風險案例。
Rootpipe
在 2014 年,研究人員向(當時還被叫做)OS X 系統報告了一個安全問題,並命名為 Rootpipe。問題組件出在 writeconfig 的 XPC 服務中。這個服務提供了一個可以寫入任意內容到任意路徑,同時設置文件屬性的遠程接口。
這個接口沒有做任何校驗,任何第三方應用程式都可以調用。寫入文件屬性的參數可以設置 suid 位,因此直接創建一個具有 suid 權限的可執行文件,其再調用 setuid 即可提升到 root。
校驗 peer
macOS 針對此類問題採用的策略是限制可訪問 XPC 服務的 peer 的代碼籤名,首先要求對方數字籤名來自 Apple,有時候還會要求具有特定的 entitlement。
XPC 主要有兩種風格的 API,一種是面向過程的 C API,在此之上又封裝了支持複雜 Objective-C 對象序列化的 NSXPC 系列 API。
面向過程的 API 使用 xpc_connection_set_event_handler 設置一個 block(Objective-C 的匿名函數)來處理 connection 的各種事件。當 xpc_get_type(event) 返回 XPC_TYPE_CONNECTION 時,意味著連接剛剛建立。服務可以通過驗證 peer 的有效性來決定是 xpc_connection_resume 繼續通信,還是忽略連接。
如果是基於 NSXPC,遠程調用被封裝到 NSXPCListener 類中。校驗 peer 的時機在 delegate 類的 listener:shouldAcceptNewConnection: 方法。假如校驗合法,則繼續連接並返回 YES。
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection { connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)]; connection.exportedObject = [[HelperTool alloc] init]; [connection resume]; return YES;}坑
mac 的文檔在這裡留了一個大坑,就是沒有說明到底應該怎麼校驗傳入連接的合法性。
在 SMJobBless 的官方實例 EvenBetterAuthorizationSample 裡寫了一句非常誤導人的描述。代碼在程序的 Info.plist 裡設置了一個 SMAuthorizedClients 鍵值標記允許的代碼籤名要求(Code Signing Requirement)
https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html
Code Signing Requirement 是一個領域特定語言(DSL),用來描述代碼籤名所需要滿足的條件。示例如下:
anchor apple and info [CFBundleShortVersionString] < "17.4"這個示例讓開發者認為只要簡單在 Info.plist 裡設置好這個值即可。然而事實上這是需要開發者編寫代碼去實現如下流程:
也就是說僅僅設置 Infp.plist 是沒有任何效果的。
另一個更大的坑來自如何正確地獲取 peer 連接的 SecCodeRef。
SecCodeCopySelf 指向進程自身,沒有檢查的意義。
而 SecStaticCodeCreateWithPath 校驗的是文件,存在可運行時替換的問題。由於 mac 系統不像 Windows 那樣鎖定運行中的可執行文件,完全可以先執行惡意程序 A,然後將自身路徑替換成可信的程序 B 而不影響運行。這樣嘗試檢查合法性會誤認為有效。這個函數僅在運行程序之間有意義。
SecCodeCopyGuestWithAttributes 支持傳入一個 CFDictionaryRef,鍵名包括:
https://developer.apple.com/documentation/security/code_signing_services/guest_attribute_dictionary_keys?language=objc
一個示例如下:
SecCodeRef code = NULL;NSDictionary *attributes = @{ kSecGuestAttributePid: @connection.processIdentifier};
SecCodeCopyGuestWithAttributes(0, attributes, kSecCSDefaultFlags, &code);
這裡採用了對方進程的 pid 作為參數,看上去沒有什麼問題,甚至 mac 自己的服務也這麼寫。CVE-2019-8565
進程 pid 實際上在 XPC 驗證 peer 的場景中不能信任。筆者在 macOS High Mojave 上找到的漏洞證明,即使是 Apple 自己也被這個 API 坑了。
漏洞在 macOS 10.14.4 被修復。漏洞根源在於 com.apple.appleseed.fbahelperd 這個 XPC 服務。在處理傳入連接的時候,進程直接使用 pid 作為參數校驗。
實際上在 Unix 系統中 pid 的個數是有上限的,但進程的創建和終止讓達到這個上限並非難事。因此作業系統有 pid 復用的機制,在不同時刻,同一個 pid 可能對應了不同的進程。
除了將用過的 pid 回收,還有一個技巧可以主動替換 pid 對應的進程,就是 exec 函數。一般使用 exec 執行程序之前都會 fork,但假如故意忽略掉 fork,就會出現 pid 保持原樣,進程卻被整個替換成全新的命令的情況。使用 posix_spawn 函數的 POSIX_SPAWN_SETEXEC 標誌位也可以實現同樣效果。
XPC 在處理來自不同進程的請求時復用了同一個消息隊列,這就給條件競爭攻擊留下了空間。
攻擊程序創建十個左右完全一致的子進程,同時向 XPC 服務發起請求。使用非阻塞函數發送消息之後,迅速調用 exec / posix_spawn 將自身進程替換成籤名合法的系統自帶程序:
#define COUNT 10int pids[COUNT];for (int i = 0; i < COUNT; i++) { int pid = fork(); if (pid == 0) { xpc_connection_t connection = xpc_connection_create_mach_service("Helper", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED); xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {}); xpc_connection_resume(connection); xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0); xpc_connection_send_message(connection, message); char* target_binary = "/path/to/valid signed binary"; char* target_argv[] = {target_binary, NULL}; exec_blocking(target_binary, target_argv, environ); } else { pids[i] = pid; }}sleep(1);for (int i = 0; i < COUNT; i++) { pids[i] && kill(pids[i], 9);}這樣一來,總有一些請求正好繞過代碼籤名檢查,得以訪問受限制的 XPC 服務接口。在結合 fbehelper 服務本身的路徑穿越等缺陷,可以在毫秒級別獲得 root 權限。
auditToken
既然 pid 不可靠,在需要安全檢查的場景又該如何處理?
在 XNU 裡有一個 audit_token_t 結構,除了 pid 之外還保存了其他欄位,特別是 p_idversion,用來保證相同的 audit_token_t 只能表示同一個進程:
audit_token.val[0] = my_cred->cr_audit.as_aia_p->ai_auid;audit_token.val[1] = my_pcred->cr_uid;audit_token.val[2] = my_pcred->cr_gid;audit_token.val[3] = my_pcred->cr_ruid;audit_token.val[4] = my_pcred->cr_rgid;audit_token.val[5] = p->p_pid;audit_token.val[6] = my_cred->cr_audit.as_aia_p->ai_asid;audit_token.val[7] = p->p_idversion;校驗代碼籤名的時候用 audit_token_t 比 pid 更為安全(雖然 Project Zero 後來在這裡又找了一個漏洞),但是蘋果看上去不想讓第三方開發者用這個結構。以下兩個函數分別可以在面向過程和 NSXPC 的接口中獲取 audit token,但它們既沒有文檔也沒有在頭文件中提供:
macOS 自帶的服務大量使用這兩個接口做安全檢查,但它們卻不向第三方軟體提供。這就導致第三方應用不停地出現類似的安全問題:Google Chrome CVE-2020-6574
Security: Keystone for macOS should use auditToken to validate incoming XPC messages
https://bugs.chromium.org/p/chromium/issues/detail?id=1102196KeyBase(KB004)
還被 35c3 做成了真實 CTF 題Office Mac 版(CVE-2018-8412)
VMware Fusion (CVE-2018-6962)
Adobe CreativeCloud (CVE-2018-4991)
解決方案
回到一開始我們關注到的這個新函數 xpc_connection_set_peer_code_signing_requirement。這個函數的第二個參數接受一個字符串,也就是前文提到的 Code Signing Requirement 語言。
筆者現在其實沒有下最新版的 Beta 系統來驗證具體的代碼實現,不過相信 Apple 此舉是為了解決代碼校驗這個大坑,隱藏具體的 audit_token_t 和 SecCodeRef 等細節,直接讓開發者傳入一個字符串來限制所期望的籤名規則。
未完待續……
不過就算代碼籤名得到解決,其實在某些情況下還是有辦法繞過。首先受信任的 peer 必須開啟 Hardened Runtime,以避免代碼注入的問題
https://developer.apple.com/documentation/security/hardened_runtime
曾經只需要一個 DYLD_INSERT_LIBRARIES 環境變量就可以向命令注入一個 dylib 庫,或者使用其他 dylib 劫持的方式觸發 dlopen 載入惡意代碼。這樣一來校驗 peer 是在主進程上做的,並沒有遞歸對所有的動態庫做檢查。惡意注入的代碼模塊可以藉此繞過檢測,包括最新的 xpc_connection_set_peer_code_signing_requirement API 也有被過的可能。
另外一種情況就是代碼重用問題。例如經典的 return oriented programming,在沒有控制流保護的平臺上一但出現本地可以觸發的代碼執行漏洞,就可以完全以受信任程序的身份執行任意代碼。由於確實沒有引入新的可執行文件,無從檢測。
因此如果有需要調用 XPC Helper 的程序,一定要避免信任腳本解釋器、Electron 等天生就為了執行代碼而設計的框架。框架本身可能存在腳本調用 dlopen 的可能性,此外類似 v8 等腳本引擎還容易受到 patchgap 導致的 nday 攻擊。通過復用代碼冒充可信代碼籤名的繞過基本無解,只能儘量避免出現類似問題。
參考資料
xpc_connection_set_peer_code_signing_requirement https://developer.apple.com/documentation/xpc/3755524-xpc_connection_set_peer_code_sig?language=objc
ObjC 中國 - XPC https://objccn.io/issue-14-4/
Rootpipe - WIkipedia https://en.wikipedia.org/wiki/Rootpipe
The Story Behind CVE-2019-13013 https://blog.obdev.at/what-we-have-learned-from-a-vulnerability/
Code Signing Requirement Language https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html
Hardended Runtime https://developer.apple.com/documentation/security/hardened_runtime