前段時間調研 Android 後臺啟動 Activity 的方案,參考 實戰|Android後臺啟動Activity實踐之路 一文,當時的結論如下:
原生Android ROM
Android 原生 ROM 都能正常地從後臺啟動 Activity 介面,無論是 Android 9(直接啟動) 還是 10 版本(藉助全屏通知)。
定製化ROM
Android P版本的機型:
透過 moveTaskToFront 方法將應用切換到前臺,如果切換失敗的話可以多嘗試幾次呼叫 moveTaskToFront 方法;小米機型可以透過Hook相關引數來後臺啟動Activity;Android Q版本的機型:
透過系統全屏通知的方式調起後臺 Activity;在一些另作了限制的 ROM 上可能調起失敗;後來又想到如果能夠拿到這些機型 ROM 的原始碼,那麼透過閱讀 startActivity 以及後臺啟動許可權設定頁面的原始碼,那麼就有可能找到破解的方法。至於怎麼獲取 ROM 的原始碼,我這裡有兩種方式:
如果手裡有現成的機型,則可以直接將 /system/framework/ 中的內容透過 adb pull 命令拉下來,然後透過一些反編譯工具可以查閱相關的原始碼;去相關廠商的官網下載對應機型的 ROM 包,透過工具將其解壓轉換,最終也可以拿到原始碼。這篇文章主要是延續上篇文章的內容,介紹一下怎麼拿到 ROM 包原始碼,並以小米某機型為例,找出它們針對後臺啟動許可權所做的定製化。
獲取ROM原始碼adb pull我一開始也沒想到原來不需要 root 許可權即可從手機裡 pull /system/framework/ 裡的內容,方式也很簡單,手機連線電腦後執行命令即可在當前路徑下拿到 framework 資料夾內容:
$ adb pull /system/framework
這種方式做過 Android 開發的應該都知道,就不再多說了。不過需要注意的是在一些機器裡 pull 下來的 framework 資料夾下的 jar 檔案可能都是隻有 1Kb 的大小,這種 jar 檔案裡不含有原始碼,在 framework 下還有一些 odex 檔案,需要將其轉換成 dex 等格式才更好反編譯,具體怎麼轉換的可以網上搜,貌似還有挺多教程的。
解壓ROM首先去對應廠商的官網下載 ROM 包,以小米為例是在 MIUI下載 裡下載,下載了目標 ROM.zip 後將其解壓縮,我下載的是小米 Max3(Android 9) 的 ROM 包,解壓後我們需要的有兩個檔案: system.new.dat.br 和 system.transfer.list。接下來分步驟看看怎麼反編譯出它的原始碼:
下載 ROM 製作工具: 下載地址,下載安裝開啟後,選擇其 實用工具 欄,然後開啟 new.dat編輯 功能,如下圖:按照下圖的兩個步驟轉換。首先第一步選擇 system.new.dat.br 檔案轉換,得到 .new.dat 字尾的 system.new.dat 檔案;然後第二步選擇這個 system.new.dat 檔案轉換,可能提示需要 .transfer.list 檔案,直接選擇上面的 system.transfer.list 檔案即可,轉換後會得到一個 img 字尾的檔案,將其解壓縮。開啟解壓縮後的資料夾,進入 /system/framework/ 目錄下,即可看到我們需要的 jar 檔案們。反編譯ROM原始碼這一步是要將上面得到的 jar 或者 dex 檔案反編譯得到原始碼,網上有很多介紹反編譯的文章,也有很多工具比如說 apktool, dex2jar, jd-gui 等,這裡介紹一個傻瓜式操作的工具——jadx,如果想要省事的話可以直接使用這個工具,它可以直接開啟 jar, dex, apk 等字尾的檔案,直接檢視反編譯後的原始碼,是不是很方便呢?
另外如果要使用 jd-gui 檢視的話,網上有很多教程了,或者直接 --help 檢視相關工具的 Usage。
後臺啟動許可權做了什麼?經過上面的步驟我們得到了 ROM 反編譯後的原始碼,這一章開始進入具體的原始碼分析流程。從之前 實戰|Android後臺啟動Activity實踐之路 可以知道,當我們呼叫 startActivity 後,會來到 AMS 這一端,AMS 進行了一些處理後,會呼叫到 ActivityStarter.startActivity 方法,對這個流程有疑問的可以看看 Activity 的啟動流程,可以參考 Android-Activity啟動流程。檢視反編譯後的程式碼,發現裡面呼叫了小米自定義的 Inject 類中的靜態方法(在 services.jar 中):
private int startActivity(IApplicationThread paramIApplicationThread, Intent paramIntent1, ...) { // ... // 這個 i 在 AOSP 原始碼中原名叫 boolean abort i = activityStackSupervisor.checkStartAnyActivityPermission(...) ^ true | this.mService.mIntentFirewall.checkStartActivity(...) ^ true; paramInt1 = i; if (i == 0) { // 表示 !abort paramInt1 = i; if (!ActivityTaskManagerServiceInjector.isAllowedStartActivity(this.mService, this.mSupervisor, paramIntent1, ...)) paramInt1 = 1; } // 根據上面的bool值判斷是否接著執行 startActivity 流程 // ...}
其實這樣的 XXXInject 類在原始碼中還有很多,都是用來做一些自定義邏輯的,我們重點看下這個 ActivityTaskManagerServiceInjector.isAllowedStartActivity() 方法的邏輯:
static boolean isAllowedStartActivity(..., Intent paramIntent, ...) { StringBuilder stringBuilder; // 1 if (UserHandle.getAppId(paramInt) == 1000 || (paramIntent.getMiuiFlags() & 0x2) != 0 || PendingIntentRecordInjector.containsPendingIntent(paramString) || PendingIntentRecordInjector.containsPendingIntent(paramActivityInfo.applicationInfo.packageName) || paramInt == mLastStartActivityUid || paramActivityTaskManagerService.isUidForeground(paramInt)) { return true; } // 2 if (paramActivityTaskManagerService.mWindowManager.isKeyguardLocked() && paramActivityTaskManagerService.getAppOpsService().noteOperation(10020, paramInt, paramString) != 0) { stringBuilder = new StringBuilder(); stringBuilder.append("MIUILOG- Permission Denied Activity KeyguardLocked: "); // ... Slog.d("ActivityTaskManagerServiceInjector", stringBuilder.toString()); return false; } // ... // 3 if (stringBuilder.getAppOpsService().checkOperation(10021, paramInt, paramString) != 0) { SparseArray<WindowProcessController> sparseArray = ((ActivityTaskManagerService)stringBuilder).mProcessMap.getPidMap(); for (int i = sparseArray.size() - 1; i >= 0; i--) { int j = sparseArray.keyAt(i); WindowProcessController windowProcessController = (WindowProcessController)sparseArray.get(j); if (windowProcessController != null && windowProcessController.mUid == paramInt && (windowProcessController.hasForegroundActivities() || (ExtraActivityManagerService.isProcessRecordVisible(j, paramInt) && windowProcessController.hasActivities() && paramInt == activityRecord.launchedFromUid))) { mLastStartActivityUid = paramActivityInfo.applicationInfo.uid; return true; } } stringBuilder.getAppOpsService().noteOperation(10021, paramInt, paramString); stringBuilder = new StringBuilder(); stringBuilder.append("MIUILOG- Permission Denied Activity : "); // ... Slog.d("ActivityTaskManagerServiceInjector", stringBuilder.toString()); return false; } return true;}
接下來我們從上面標的數字講起:
首先看看數字2的部分:我們看到了這裡有一個 OpCode=10020, 這個 Code 對應的許可權也是小米增加的,看下面的日誌可以知道這個 Code 就是我們常看到的小米鎖屏顯示的許可權,由此可以知道如果我們呼叫 startActivity 時手機沒有解鎖,那麼會走到這個流程,判斷應用有沒有這個 10020 的許可權,如果有則接著往下走,如果沒有許可權則直接返回 false 表示不能啟動目標 Activity。然後看數字3的部分:跟上面類似,它處理 OpCode=10021 的許可權鑑定,這個值跟我們在前一篇文章裡講到的後臺啟動許可權的 Code 是一樣的!也就是說這段程式碼就是用來判斷應用有沒有後臺啟動的許可權的。至於這兩個許可權相關的日誌:Permission Denied Activity KeyguardLocked: ... 和 Permission Denied Activity: ...,我們在遇到這兩種場景後,在 logcat 中過濾 MIUILOG Tag 是可以看到這兩種日誌輸出的,有興趣的同學可以驗證一下~
接下來再看數字1部分,這裡就是我們繞過這兩個許可權的關鍵!它在這個方法的開頭,如果這個 if 判斷為真的話則會直接返回 true,從而跳過後面許可權認證的邏輯,我們重點關注 (paramIntent.getMiuiFlags() & 0x2) != 0 這個判斷條件:由此可以看出小米在 Intent 類中增加了一個形如 MiuiFlags 的標誌位,我們開啟 Intent 類看看具體情況,Intent 類在 framework.jar 中:
public class Intent implements Parcelable, Cloneable { private int mMiuiFlags; // ... public Intent addMiuiFlags(int flags) { this.mMiuiFlags |= flags; return this; } public Intent setMiuiFlags(int flags) { this.mMiuiFlags = flags; return this; } public int getMiuiFlags() { return this.mMiuiFlags; }}
果然,Intent 中被增加了一個標誌位,那麼我們估計就知道怎麼去解決這個問題了,那就是在小米平臺上透過反射將我們的 Intent 引數中的 mMiuiFlags 設定成 0x2 即可繞過這兩個許可權的認證!
另外這裡要注意一個問題:在 Android 9 以上 Intent 類中的屬性是不能被反射的,因此我們需要想辦法解決這個問題,網上已經有了許多現成的方式,這裡我就不做展開了,想了解具體原理的直接 Google 即可。我借用了 Github 上的一個開源庫——FreeReflection,透過它可以方便地防止反射 Intent 丟擲異常崩潰。
經過實際測試,當我將 Intent 中的這個屬性修改成 0x2 以後,可以直接從後臺或在鎖屏時啟動我們應用的 Activity。
總結其實整體來說這套解決方案還是挺簡單的,在找到工具反編譯 ROM 程式碼後,熟悉 Activity 啟動原始碼的同學還是能比較輕鬆地找到其中的突破點的,當然中間可能會走錯方向,像我有時候就容易盯著一個跟目標毫無關聯的方法看,因為不能確定到底哪裡才是真正的關鍵點。猜測其他版本的小米機器應該都是用的這種方式,畢竟同一個廠商沒必要弄多套方案去做這個許可權的功能。
參考上述方式,如果對於一些廠商 ROM 的定製化功能有疑問或者開發中有這種奇怪Bug(與廠商定製相關)的,都可以從它們的原始碼中找到蛛絲馬跡,也算是一種解決思路吧,時間足夠的話,可以自己直接從原始碼中尋找答案,不然在網上搜來搜去的,有的能找到答案那是萬幸,有的則完全不知所云暈頭轉向的。
最後在這裡我也分享一份由幾位大佬一起收錄整理的Flutter進階資料以及Android學習PDF+架構影片+面試文件+原始碼筆記,高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料。
這些都是我閒暇時還會反覆翻閱的精品資料。可以有效地幫助大家掌握知識、理解原理。當然你也可以拿去查漏補缺,提升自身的競爭力。