另一種繞過 Android P以上非公開API限制的辦法
去年釋出的 Android P上引入了針對非公開API的限制,對開發者來說,這絕對是有史以來最重大的變化之一。前天 Google 釋出了 Android Q 的 Beta 版,越來越多的 API 被加入了黑名單,而且 Google 要求下半年 APP 必須 target 28,這意味著現在的深灰名單也會生效;可以預見,在不久的將來,我們要跟大量的 API 說再見了。
去年我給出了 一種繞過Android P對非SDK介面限制的簡單方法 ,經驗證,這辦法在 Android Q 的 Beta 版上依然能正常使用。雖然這個方法需要進行記憶體搜尋,理論上有可能失敗,但實際上它曾在 VirtualXposed 和 太極 中得到了較為廣泛的驗證,從未收到過由於反射失敗而導致問題的反饋。而且據我所知,有若干使用者量不少的 APP 在線上使用了我提供的 tiann/FreeReflection 庫,想來應該也是沒有問題的吧。
不過今天,我打算給出另外一種繞過限制的辦法。這個辦法目前來說是最優方案,我個人使用了一個多月,不存在任何問題。
上次 分析系統是如何施加這個限制 的時候,我們提到了幾種方式,最終給出了一種修改 runtime flag 的辦法;其中我們提到,系統有一個 `fn_caller_is_trusted` 條件:** 如果呼叫者是系統類,那麼就允許被呼叫 **。這是顯而易見的,畢竟這些私有 API 就是給系統用的,如果系統自己都被拒絕了,這是在玩錘子呢?
也就是說,如果我們能** 以系統類的身份去反射,那麼就能暢通無阻 **。問題是,我們如何以「系統的身份去反射」呢?一種最常見的辦法是,我們自己寫一個類,然後通過某種途徑把這個類的 ClassLoader 設定為系統的 ClassLoader,再借助這個類去反射其他類。但是這裡的「通過某種途徑」依然要使用一些黑科技才能實現,與修改 flags / inline hook 無本質區別。
** 以系統類的身份去反射 ** 有兩個意思,1. 直接把我們自己變成系統類;2. 藉助系統類去呼叫反射。我們一個個分析。
「直接把我們自己變成系統類」這個方式有童鞋可能覺得天方夜譚,APP 的類怎麼可能成為系統類?但是,一切皆有可能!我們知道,對APP來說,所謂的系統類就是被 BootstrapClassLoader 載入的類,這個 ClassLoader 並非普通的 DexClassLoader,因此我們無法通過插入 dex path的方式注入類。但是,Android 的 ART 在 Android O 上引入了 JVMTI,JVMTI 提供了將某一個類轉換為 BootstrapClassLoader 中的類的方法!具體來說,我們寫一個類暴露反射相關的介面,然後通過 JVMTI 提供的 `AddToBootstrapClassLoaderSearch`將此類加入 BootstrapClassLoader 就實現目的了。不過,JVMTI 要在 release 版本的 APP 上執行依然需要 Hack,所以這種途徑與其他的黑科技無本質區別。關於 JVMTI 可以參考我這個回答:
Android Studio 3.5提供的”Apply Changes“是什麼原理? www.zhihu.com第二種方法,「藉助系統的類去反射」也就是說,如果系統有一個方法`systemMethod`,這個`systemMethod` 去呼叫反射相反的方法,那麼`systemMethod`毋庸置疑會反射成功。但是,我們從哪去找到這麼一個方法給我們用?事實上,我們不僅能找到這樣的方法,而且這個方法能幫助我們呼叫任意的函式,那就是** 反射本身! **可能你已經繞暈了,我解釋一下:
- 首先,我們通過反射 API 拿到 `getDeclaredMethod` 方法。`getDeclaredMethod` 是 public 的,不存在問題;這個通過反射拿到的方法我們稱之為** 元反射方法 **。
- 然後,我們通過剛剛反射拿到** 元反射方法 **去反射呼叫 `getDeclardMethod`。這裡我們就實現了** 以系統身份去反射 **的目的——反射相關的 API 都是系統類,因此我們的元反射方法也是被系統類載入的方法;所以我們的元反射方法呼叫的 `getDeclardMethod` 會被認為是系統呼叫的,可以反射任意的方法。
虛擬碼如下:
Method metaGetDeclaredMethod = Class.class.getDeclaredMethod("getDeclardMethod"); // 公開API,無問題 Method hiddenMethod = metaGetDeclaredMethod.invoke(hiddenClass, "hiddenMethod", "hiddenMethod引數列表"); // 系統類通過反射使用隱藏 API,檢查直接通過。 hiddenMethod.invoke // 正確找到 Method 直接反射呼叫
到這裡,我們已經能通過「元反射」的方式去任意獲取隱藏方法或者隱藏 Field 了。但是,如果我們所有使用的隱藏方法都要這麼幹,那還有點小麻煩。在上文 中,我們後來發現,隱藏 API 呼叫還有「豁免」條件,具體程式碼如下:
if (shouldWarn || action == kDeny) { if (member_signature.IsExempted(runtime->GetHiddenApiExemptions())) { action = kAllow; // Avoid re-examining the exemption list next time. // Note this results in no warning for the member, which seems like what one would expect. // Exemptions effectively adds new members to the whitelist. MaybeWhitelistMember(runtime, member); return kAllow; } // 略 }
只要 `IsExempted` 方法返回 true,就算這個方法在黑名單中,依然會被放行然後允許被呼叫。我們再觀察一下`IsExempted`方法:
bool MemberSignature::IsExempted(const std::vector<std::string>& exemptions) { for (const std::string& exemption : exemptions) { if (DoesPrefixMatch(exemption)) { return true; } } return false; }
繼續跟蹤傳遞進來的引數 `runtime->GetHiddenApiExemptions()` 發現這玩意兒也是 runtime 裡面的一個引數,既然如此,我們可以一不做二不休,仿照修改 runtime flag 的方式直接修改 `hidden_api_exemptions_` 也能繞過去。但如果我們繼續跟蹤下去,會有個有趣的發現:這個API 竟然是暴露到 Java 層的,有一個對應的 VMRuntime.setHiddenApiExemptions Java方法;也就是說,只要我們通過 `VMRuntime.setHiddenApiExemptions` 設定下豁免條件,我們就能愉快滴使用反射了。
再結合上面這個方法,我們只需要通過 「元反射」來反射呼叫 `VMRuntime.setHiddenApiExemptions` 就能將我們自己要使用的隱藏 API 全部都豁免掉了。更進一步,如果我們再觀察下上面的 `IsExempted` 方法裡面呼叫的 `DoesPrefixMatch`,發現這玩意兒在對方法簽名進行字首匹配;童鞋們,我們所有Java方法類的簽名都是以 L 開頭啊!如果我們把直接傳個 `L`進去,所有的隱藏API全部被赦免了!
詳細程式碼在這裡:
tiann/FreeReflection github.com
理論上講,這個方案不存在相容性問題。即使 ROM 刪掉了 `setHiddenApiExemptions` 方法,我們依然可以用「元反射」的方式去反射隱藏API,並且所有的程式碼加起來不超過30行!當然,如果 Google 繼續改進驗證隱藏API呼叫的方法,這個方式可能會失效;但是目前的機制沒有問題。
文章的最後,我想說的是,本文的目的不是刻意去繞過限制。不給思維設限,才會有更多可能。