看到這個標題我覺得某司的程序員又要緊張一下了,怎么好不容易搞出了個凍結反彈又被人搞了。恩,要搞的就是這種流氓行為。
首先來看一下具體的現象,所謂的凍結反彈,就是當你使用pm disable
使一個 APP 處于凍結狀態后,重啟手機,APP 自動解凍了。典型的例子就是 MIUI 內置的音樂、視頻等。另外還有刪除指定 APP 重啟就卡 MIUI Logo 不能進系統的,這對于取了 root 想自己改改系統的人來說,簡直不可忍。
那么廢話不多說,直接來看解決方案,可靠的解決方案有三種,BOOT_COMPLETED
消息,修改service.jar/service.odex
,使用Xposed
動態修改。
方法一
第一種是最簡單的,維護一個列表,當有 APP 被凍結或解凍時,即修改列表內成員,在隨后的重啟過程中,接收BOOT_COMPLETED
消息,并對列表內的 APP 再次進行凍結,具體的代碼是這樣的:
class BootReceiver : BroadcastReceiver() {
companion object { var inited = false }
override fun onReceive(context: Context?, intent: Intent?) {
if (!inited) {
inited = true
val pref = context?.getSharedPreferences(XpStatus.PREF, 0)
if (pref!!.getBoolean(XpStatus.KEY_PREVENT_FREEZE_REVERSE, false))
context?.startService(Intent(context, FreezeService::class.java))
}
}
}
class FreezeService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
thread { NativeAPI.freezeOnLoad() }
return super.onStartCommand(intent, flags, startId)
}
}
同時改一下 AndroidManifest.xml
就好:
<service android:name=".FreezeService"/>
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
這個情況下,會遇到兩個坑,其一就是在 MIUI 上,必須允許這個 APP 自啟,同時它也不能被綠色守護,阻止運行等 APP 管理,否則會收不到BOOT_COMPLETED
消息;第二個坑也是在 MIUI 上,BOOT_COMPLETED
收到的時機問題,有可能是在手機啟動后 1 分鐘才收到該消息,于是就會出現用戶以為 APP 自動解凍了,但是過了一陣子那個 APP 又消失(被凍結)了,給用戶非常不好的體驗。
第一個問題,無解,這是小米所設計的機制,繞不過去,可能對于部分用戶來說,好不容易能把 APP 的自啟都干掉了,結果對于這個 APP 又要給自啟權限,非常的不爽。第二個問題在 6.0 和以下版本的 MIUI 中是可以解的,解法就是加入對AUDIO_BECOMING_NOISY
消息的監聽:
<receiver android:name=".receiver.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY"/>
</intent-filter>
</receiver>
AUDIO_BECOMING_NOISY
發送的時機遠比BOOT_COMPLETED
更早,在桌面啟動前,就可以收到這個消息,在此處進行對 APP 的重新凍結是靠譜的。但是需要注意的是,Android N以后,啟動時不再發送此消息,因此這個做法在 Android N 為基礎的 MIUI 上是無效的。
同時也再說一句,同樣是 Android,CM 的 ROM 就不會有BOOT_COMPLETED
延遲,也無需進行允許自啟這樣的操作。
方法二
直接修改service.jar/service.odex
也是個不錯的方案,反正都已經獲取到 root 了,可以隨意替換。最終修改的是 jar 還是 odex,取決于你從 ROM 里面拿到的文件是哪個,它們基本上沒差別。
為了方便起見,我直接給出反編譯后的 java 代碼,可以參照著修改 smali 代碼:
package com.miui.server;
... ...
public class SecurityManagerService extends Stub {
... ...
private void checkSystemSelfProtection(final boolean onlyCore) {
... ...
while (i$.hasNext()) {
SecurityManagerService.this.checkEnabled(pm, (String) i$.next());
}
... ...
if (!(Build.IS_INTERNATIONAL_BUILD || Build.IS_CM_CUSTOMIZATION || Build.IS_CM_CUSTOMIZATION_TEST)) {
SecurityManagerService.this.enforceAppSignature(platformSignature, "com.xiaomi.market", false);
}
... ...
if (!SecurityManagerService.this.checkSysAppCrack()) {
i = 0;
}
... ...
}
... ...
}
代碼太長,就貼這些關鍵的,它們完成了對已凍結 APP 的重新啟用,和對于刪除市場后直接卡 MIUI Logo 的處理,不得不說,這些代碼實在是惡心。
在 smali 內,可以將checkSystemSelfProtection
方法整個置空,或是修改checkEnabled
,enforceAppSignature
,checkSysAppCrack
這三個函數,刪除函數體,或是使其返回你要的值。完成后重新打包,并替換回系統的 framework 內即可。
使用這個方法的坑在于,如果系統更新了,你就必須重新做這一系列步驟,當然這一塊代碼可讀性還是比較強的,過于底層的東西很難進行混淆處理,改起來比較方便。
方法三
最終,最完美的方法還是要借助 Xposed 框架,一勞永逸的解決這些問題,不多說,直接給代碼了:
if (param.packageName == "android"
|| param.packageName == "com.miui.system"
|| param.packageName == "miui.system") {
val clsSMS = XpUtils.findClass(param.classLoader, "com.miui.server.SecurityManagerService")
if (clsSMS != null) {
XpUtils.findAndHookMethod("com.miui.server.SecurityManagerService", param.classLoader, "checkSysAppCrack", XC_MethodReplacement.returnConstant(false))
XpUtils.findAndHookMethod("com.miui.server.SecurityManagerService", param.classLoader, "checkEnabled", PackageManager::class.java, String::class.java, XC_MethodReplacement.returnConstant(null))
XpUtils.findAndHookMethod("com.miui.server.SecurityManagerService", param.classLoader, "enforcePlatformSignature", Array<Signature>::class.java, XC_MethodReplacement.returnConstant(null))
}
}
現在,又可以享受凍結不會反彈的 MIUI 了。
后記:
其實在研究的過程中,踩過的坑遠遠不止這三種,Xposed 還有以下的大坑,開發時需注意:
- 不能使用對應 APP 內的 JNI 庫,因為不在同一進程,如果非要用的話,必須事先將對應架構的 JNI 庫置入
/system/lib
或/vendor/lib
內 - 不能在Xposed 內調用 su,因為 Xposed 執行的時候,su 所對應的上層應用還沒準備好,因此 root 請求會被直接拒絕,從而產生一個 permission denied 異常
- 不能在 Xposed 內訪問 APP 所對應的 /data/data/ 內的數據,Xposed 進程并沒有這樣的權限,甚至簡單的判斷文件是否存在都只會返回 false