天天看點

一位對抗蘋果的“勇士”:公開 iOS 未修複漏洞後,再寫《惡意軟體進 App Store 指南》

被稱為“圍牆花園”的蘋果,一直以來都是以“安全”為由堅持着各種外界看來十分霸道的規則:為了安全,禁止第三方換産品電池;為了安全,不同意開放第三方應用商店;為了安全,不允許“側載”應用…

一切都為了安全,一切都為了使用者,既然如此,為何 App Store 裡惡意軟體層出不窮?為何近日有安全研究人員稱蘋果長期漠視 iOS 15 中仍然存在的三個零日漏洞?

對此,開發者 Denis Tokarev 否定了 App Store 安全的說法,并詳細介紹了惡意軟體進入 App Store 的過程,直言道:“蘋果不允許任何 App Store 替代品的真正原因是,這樣他們就能從所有應用内購中抽取 30% 的傭金,這對他們來說是一項非常有利可圖的業務。”

一位對抗蘋果的“勇士”:公開 iOS 未修複漏洞後,再寫《惡意軟體進 App Store 指南》

一、被态度敷衍的蘋果激怒

Denis Tokarev 之是以突然如此憤慨,是因為就是他在今年 3~5 月陸續向蘋果報告了 iOS 中存在的 4 個零日漏洞。蘋果曾向他承諾,會将這些漏洞加進安全内容清單,但卻食言了。

4 個漏洞中,蘋果隻悄悄地在 iOS 14.7 更新中修複了其中一個漏洞,并且沒有将其加入安全内容清單,至于另外 3 個零日漏洞,直到 iOS 15 中還依舊存在。

蘋果的這番舉動令 Denis Tokarev 十分不滿,他認為蘋果不僅對系統漏洞的發現态度敷衍,還掩蓋了它在 iOS 14.7 更新中修複漏洞的行為,而這一切,都是為了避免向 Denis Tokarev 支付根據其安全賞金計劃應得的 10 萬美元。

Denis Tokarev 試圖就此問題與蘋果進行溝通,卻對方始終沒有答複,盛怒之下他寫了一篇詳細介紹那 3 個蘋果還未修複的零日漏洞以及批判其安全賞金計劃的部落格文章,3 個漏洞分别名為 Gamed、Nehelper installed apps 和 Nehelper wifi info,而 iOS 14.7 中已修複的漏洞為 Analyticsd。

在這篇文章引起公衆關注後,蘋果終于回複了 Denis Tokarev:

“我們看到了你關于此問題的博文和其他報告。對于延遲回複你,我們深表歉意,但我們隻是仍在調查并思考要如何解決這些問題以保護客戶。再次感謝你抽出時間向我們報告這些問題,我們感謝你的幫助。如果你有問題,請告訴我們。”

可是,一切都晚了,Denis Tokarev 對蘋果這種幾乎是“被迫”回應的态度再次激怒,也對有人質疑該惡意代碼是否真的能進入 App Store 的行為感到可悲:“他們這麼懷疑我是可以了解的,因為蘋果總是一遍又一遍地重複,讓人們相信 App Store 是安全的。”

是以,Denis Tokarev 再度提筆,詳細說明了惡意軟體是如何通過稽核進入 App Store 的整個過程(包括源代碼)。

二、混淆函數名稱的字元串即可

首先,Denis Tokarev 指出,當 App 的二進制檔案上傳到蘋果伺服器時,會被進行靜态分析。但其實這個過程除了根據預定義的私有 API 集(這些 API 隻有蘋果自己的 App 才允許使用)檢查二進制檔案中的字元串清單外,并不會做太多其它工作。

如果在 App 中檢測到了私有 API 的使用,蘋果就不會上傳該 App 的二進制檔案,并向 App 作者發送一封郵件:

“我們發現你最近傳遞的 App [APP_NAME_AND_VERSON] 中存在一個或多個問題,請請更正以下問題,然後重新上傳。

ITMS-90338:使用非公共 API - App 包含或繼承了 [APP_NAME]

中的非公共類:GKFamiliarPlayerInternal,GKFriendPlayerInternal,GKLocalPlayerInternal。如果你源代碼中的方法名稱與上面列出的私有蘋果

API 一緻,請更改你的方法名稱以避免該 App 在未來送出時被标記。此外,如果上述一個或多個 API 也存在于你 App

附帶的靜态庫中,那它們必須被删除。如需更多資訊,請通路

http://developer.apple.com/support/technical/。”

這種情況下,該怎麼辦呢?Denis Tokarev 給出了兩種方法:

  • 如果這是一個 Objective-C 的 API,那麼可以通過 Objective-C 運作時對其動态調用。

例如,先找到一個類GKLocalPlayerInternal(這是 Gamed 漏洞中會使用的一個類),就像 NSClassFromString(“GKLocalPlayerInternal”)。由于 GKLocalPlayerInternal 屬于私有 API,是以在分析二進制檔案時會被發現。但是,開發者可以通過多種方式對它進行隐藏,比如,可以将 GKLocalPlayerInternal 簡單分為幾個部分:NSClassFromString([“GKLoc”,“lPlayerInternal”].joined(separator: “a”)),這樣就不會被靜态分析檢測到。

Gamed 漏洞就是通過這種方法混淆了所有私有 API 的使用,是以在接受蘋果靜态分析時未被發現。

  • 使用凱撒密碼(這是一種替換加密的技術,明文中的所有字母都在字母表上向後或向前按照一個固定數目進行偏移後被替換成密文)。

Denis Tokarev 表示,他發現 App Store 上有一個下載下傳量高達幾億次的 App 就采用了這種方法。該 App 支援 iOS 9,是以開發者必須使用私有 API 來解決 UIKit 漏洞,并為那些無法安裝最新 iOS 版本的人改善體驗(蘋果認為有些裝置已過時,是以放棄了對它們的支援)。

舉個例子,自 iOS 7 開始,蘋果就為 App 圖示使用了一種特殊的圓角設計,并将這種設計覆寫到了 App 中包括按鈕、警報等所有 UI 元件。但是,這種圓角設計隻在 iOS 13 中為第三方開發者提供,同時蘋果早在 iOS 11 中就開始調用 CALayer 類的私有方法 setContinuousCorners 用于自己的 App 和系統元件,是以,如果應用開發者想讓舊 iOS 版本的使用者體驗到一緻的 App 界面,就必須使用私有 API,而這違反了 App Store 規則。

除了 Gamed 漏洞,Denis Tokarev 另外發現的三個漏洞(Analyticsd、Nehelper installed apps 和 Nehelper wifi info)都使用了蘋果認為是私有 API 一部分的 C 函數,而他已經更新了這三個漏洞的源代碼,使其能動态調用這些函數,使躲過蘋果的靜态分析。

三、實操過程及代碼

理論有了,接下來,Denis Tokarev 分享了要如何通過凱撒密碼具體實作這一過程。

首先,dlopen 和 dlsym 函數可以加載動态庫并解析其中的符号,但考慮到這可能會被 App Store 稽核團隊檢測到,是以開發者可以“曲線救國”:每個 iOS 二進制檔案都會導入一個名為 dyld_stub_binder 的符号,而它導自與 dlopen 和 dlsym 相同的庫——也就是說,開發者可以通過計算 dlopen 和 dlsym 函數在記憶體中與 dyld_stub_binder 的距離,然後僅使用這些函數的位址來調用它們。

有一點需要注意,Denis Tokarev 表示,這個距離,也就是偏移量,會根據 iOS 版本和裝置型号這兩個參數而有所變化,他在 Github 上分享的 3 個漏洞源代碼中的偏移量是對應 iPhone 7 Plus 和 iOS 15.0 的值,是以如果這與開發者的實際裝置不符,則需重新計算。

以下為計算偏移量的方法:

printf("%lld\n",(long long)dyld_stub_binder - (long long)dlopen);
printf("%lld\n",(long long)dyld_stub_binder - (long long)dlsym);
           

當有了偏移量後,開發者便可以通過定義自己的函數來調用 dlopen 和 dlsym:

// dlopen
void * normal_function1(const char * arg1, int arg2) {
    return ((void *(*)(const char *, int))((long long)dyld_stub_binder - 20780))(arg1, arg2);
}

// dlsym
void * normal_function2(void * arg1, const char * arg2) {
    return ((void *(*)(void *, const char *))((long long)dyld_stub_binder - 20648))(arg1, arg2);
}
           

在 Swift 中将其導入後,開發者便可以重寫檢查 App 是否已安裝的代碼,在這過程中無需導入和引用任何符号(除了已被二進制檔案預設導入的 dyld_stub_binder):

let dylib = normal_function1("/usr/lib/system/libxpc.dylib", 0)
let normalFunction3 = unsafeBitCast(normal_function2(dylib, "xpc_connection_create_mach_service"), to: (@convention(c) (UnsafePointer<CChar>, DispatchQueue?, UInt64) -> (OpaquePointer)).self)
let normalFunction4 = unsafeBitCast(normal_function2(dylib, "xpc_connection_set_event_handler"), to: (@convention(c) (OpaquePointer, @escaping (OpaquePointer) -> Void) -> Void).self)
let normalFunction5 = unsafeBitCast(normal_function2(dylib, "xpc_connection_resume"), to: (@convention(c) (OpaquePointer) -> Void).self)
let normalFunction6 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_create"), to: (@convention(c) (OpaquePointer?, OpaquePointer?, Int) -> OpaquePointer).self)
let normalFunction7 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_set_uint64"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>, UInt64) -> Void).self)
let normalFunction8 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_set_string"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>, UnsafePointer<CChar>) -> Void).self)
let normalFunction9 = unsafeBitCast(normal_function2(dylib, "xpc_connection_send_message_with_reply_sync"), to: (@convention(c) (OpaquePointer, OpaquePointer) -> OpaquePointer).self)
let normalFunction10 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_get_value"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>) -> OpaquePointer?).self)

func isAppInstalled(bundleId: String) -> Bool {
    let connection = normalFunction3("com.apple.nehelper", nil, 2)
    normalFunction4(connection, { _ in })
    normalFunction5(connection)
    let xdict = normalFunction6(nil, nil, 0)
    normalFunction7(xdict, "delegate-class-id", 1)
    normalFunction7(xdict, "cache-command", 3)
    normalFunction8(xdict, "cache-signing-identifier", bundleId)
    let reply = normalFunction9(connection, xdict)
    if let resultData = normalFunction10(reply, "result-data"), normalFunction10(resultData, "cache-app-uuid") != nil {
        return true
    }
    return false
}
           

簡單來說,這一切的目的就是為了不被靜态分析檢測到,是以要使用凱撒密碼混淆包含函數名稱的字元串,或者像第一種方法那樣将其分成幾塊。

Denis Tokarev 對這兩種方法非常自信:“如果蘋果敢說這樣也能在審查期間發現,那我會想出不同的應對方法并将它全部釋出出來。”

四、主觀性很強的稽核流程

在“騙”過靜态分析後,App 就會進入 App Store 的稽核流程,而 Denis Tokarev 認為在這一過程中,主觀性很強:基本上就是一個随機配置設定的測試人員把 App 下載下傳到自己的 iPad 上,然後滿螢幕地點選,再根據他們自己對 App Store 規則的了解和主觀意見做出是否允許上架的決定。

可是,這個過程具有很明顯的缺點:一般而言,惡意軟體都會連接配接到遠端伺服器,發送有關目前使用者會話的詳細資訊,并詢問是否要執行某些惡意操作。而蘋果伺服器隻會檢測蘋果測試員或普通使用者是否正在使用 App,并基于此發送響應。這意味着,稽核人員隻會看到一個外表“良善”的惡意 App,發現不了任何可疑之處,并允許它進入 App Store。

是以,這些年來 App Store 中的惡意軟體總是層出不窮,而蘋果也總是在被别人發現後才下架這些 App,甚至某些情況下還會對舉報者視若無睹:有一位開發者 Kosta Eleftheriou 曾明确指出 App Store 中存在許多帶有虛假評論的盜版 App,它們向使用者收取高昂的費用卻提供不了什麼功能,是以 Eleftheriou 想讓蘋果删除這些 App。但最終,蘋果并沒有怎麼聽取 Kosta Eleftheriou 的建議,這在 Denis Tokarev 看來,是因為蘋果也從這些盜版 App 中獲益了:高昂訂閱費中 30% 的抽成。

另外,Denis Tokarev 還例舉了一些他親身經曆過的經驗,以證明 App Store 的稽核是有多麼“主觀随性”:

  • 他送出了一款有關占星術、星座運勢、手相和算命的免費 App,卻被 App Store 稽核團隊拒絕,理由是“在 App Store 上已經有足夠多的此類 App”。可 App Store 中卻有一個 8 美元/周、帶有虛假評論的星座 App。
  • 他開發的一款名為 Hobo Simulator 的遊戲被 App Store 突然下架,理由之一是他們不喜歡“hobo”(流浪的失業勞工)這個詞以及遊戲内容,但彼時甚至直到如今 App Store 中還有許多類似應用,例如“Hobo Life”、“Hobo - Real Life Simulator”——隻有 Denis Tokarev 的“hobo”主題遊戲被下架了。
  • 在 Hobo Simulator 被下架後,有人完整克隆了 Denis Tokarev 的這款遊戲并且成功在 App Store 中上架。在 Denis Tokarev 向蘋果提出關于知識産權的投訴時,蘋果回複道 Denis Tokarev 應該自己去和那個抄襲 App 的開發者解決這個問題。
  • 2020 年 7 月,Denis Tokarev 在 App Store 中重新建立了一個 ID,并再次送出了他的“Hobo Simulator”App,而這次,App Store 允許它上架了。

是以,Denis Tokarev 表示,蘋果這些行為完全屬于反競争,并且還含有對部分開發者歧視的意味,就算最近蘋果因美國集體訴訟允許應用的第三方支付,這也還不夠。

他呼籲道,人們必須向蘋果施加壓力,讓他們開放平台,允許 App Store 替代品和側載的存在,并讓開發者獲得公平待遇:“面對壓迫和不公正,我們必須站在一起,為自由而戰!”

那麼,對于 Denis Tokarev 的做法與呼籲,你有什麼看法嗎?

參考連結:

  • https://habr.com/en/post/580272/
  • https://habr.com/en/post/579714/
  • https://en.wikipedia.org/wiki/Caesar_cipher