背景
我们在使用手机时,难免需要对某些应用做后台保活,比如社交软件、音乐播放器、VPN 代理、广告跳过工具等等,但许多国内 ROM 厂商却将 Android 的「最近任务」做成了「任务列表」,在其中侧滑或点击一键清理按钮将导致应用被杀死,使用起来很不方便。
就我这台 MIUI 而言,虽然诸如 QQ、微信 等受众较广的应用官方已经在内置的规则列表中默认为其放行,但到了 Temegram、Clash 一类的小众应用,不小心划掉之后就会比较痛苦。好在我们已经 root 了,那自然不能任由它这样下去,遂决定开发一款专为 MIUI 打造的进程锁,为我们想要的进程提供保护,使其不被清理掉
实现
逆向分析
- 以下测试均基于
MIUI V12.5.4.0.RJVCNXM
我们知道,在较新版本的 MIUI 系统中,「最近任务」属于「系统桌面」这个应用,那么首先把它提取出来,通过方法名和包名的简单搜索,很容易就可以定位到下面这个方法:
1
| com.android.systemui.shared.recents.system.ProcessManagerWrapper#doOneKeyClean(ArrayList<Task>, ArrayList<String>)
|
写个小 Hook 验证一下:
1 2 3 4 5
| findMethod("com.android.systemui.shared.recents.system.ProcessManagerWrapper") { name == "doOneKeyClean" }!! before { Log.i("called doOneKeyClean!") }
|
发现确实调用了它,接下来看看它是怎么写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void doOneKeyClean(ArrayList<Task> arrayList, ArrayList<String> arrayList2) { try { ArrayList arrayList3 = new ArrayList(); Iterator<Task> it = arrayList.iterator(); while (it.hasNext()) { arrayList3.add(Integer.valueOf(it.next().key.id)); } ProcessConfig processConfig = new ProcessConfig(1); processConfig.setRemoveTaskNeeded(true); processConfig.setRemovingTaskIdList(arrayList3); processConfig.setWhiteList(arrayList2); ProcessManager.kill(processConfig); } catch (Exception e) { Log.e(TAG, "doOneKeyClean", e); } }
|
注意到 12 行这里调用了一个 ProcessManager.kill()
方法,正准备跟进去看一下,然后发现……并没有这个类?那它一定就在 framework.jar
!到 /system/framework/
下把这个 jar 扒出来丢进 jadx,发现内部继续调用了 ProcessManagerNative.getDefault().kill()
:
1 2 3 4 5 6 7 8
| public static boolean kill(ProcessConfig config) { try { return ProcessManagerNative.getDefault().kill(config); } catch (RemoteException e) { e.printStackTrace(); return false; } }
|
看看这个 getDefault()
是如何工作的:
1 2 3 4 5 6 7 8 9 10
| public static IProcessManager getDefault() { if (pm == null) { synchronized (ProcessManagerNative.class) { if (pm == null) { pm = asInterface(ServiceManager.getService("ProcessManager")); } } } return pm; }
|
到这里已经很明显了,下一站:services.jar
!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public boolean kill(ProcessConfig config) throws RemoteException { if (checkPermission()) { int callingPid = Binder.getCallingPid(); int policy = config.getPolicy(); Slog.i("ProcessManager", "Kill reason " + getKillReason(config) + " from pid=" + callingPid); addConfigToHistory(config); this.mProcessPolicy.resetWhiteList(this.mContext, UserHandle.getCallingUserId()); boolean success = false; switch (policy) { case 1: case 2: case 4: case 5: case 14: case 15: case 16: success = killAll(config); break; case 3: case 6: case 10: success = killAny(config); break; case 7: success = swipeToKillApp(config); break; case 8: case 9: default: Slog.w("ProcessManager", "unKnown policy"); break; case 11: case 12: case 13: success = autoKillApp(config); break; case 17: success = killByPriority(config); break; } ProcessRecordInjector.reportAppPss(); return success; } String msg = "Permission Denial: ProcessManager.kill() from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid(); Slog.w("ProcessManager", msg); throw new SecurityException(msg); }
|
定位到 com.android.server.am.ProcessManagerService
,其中 kill 方法很据我们传入的 config 得到一个 policy,然后再根据 policy 决定要使用哪种 kill 手段。于是开始回溯,看看这个 ProcessConfig 从何而来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void doOneKeyClean(ArrayList<Task> arrayList, ArrayList<String> arrayList2) { try { ArrayList arrayList3 = new ArrayList(); Iterator<Task> it = arrayList.iterator(); while (it.hasNext()) { arrayList3.add(Integer.valueOf(it.next().key.id)); } ProcessConfig processConfig = new ProcessConfig(1); processConfig.setRemoveTaskNeeded(true); processConfig.setRemovingTaskIdList(arrayList3); processConfig.setWhiteList(arrayList2); ProcessManager.kill(processConfig); } catch (Exception e) { Log.e(TAG, "doOneKeyClean", e); } }
|
注意这个 new ProcessConfig(1);
中的 1 就是 policy,相应地我们可以发现下面 doSwapUPClean
(最近任务侧滑)对应的 policy 是 7:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static void doSwapUPClean(Task task) { try { String packageName = task.key.getComponent().getPackageName(); int i = task.key.userId; ProcessConfig processConfig = new ProcessConfig(7, packageName, i, task.key.id); processConfig.setRemoveTaskNeeded(true); ProcessManager.kill(processConfig); if (isHasRelatedPkg(packageName)) { Iterator<String> it = getRelatedPkg(packageName).iterator(); while (it.hasNext()) { ProcessManager.kill(new ProcessConfig(7, it.next(), i, task.key.id)); } } } catch (Exception e) { Log.e(TAG, "doSwapUPClean", e); } }
|
继续在 service.jar
中跟踪这两种 policy 的后续,发现它们最终都调用了 killOnceByPolicy()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| protected boolean killAll(ProcessConfig config) { int policy = config.getPolicy(); String reason = getKillReason(config); Map<Integer, String> fgTaskPackageMap = null; List<String> whiteList = null; if (policy != 2) { whiteList = config.getWhiteList(); if (policy == 1 || policy == 15 || policy == 4 || policy == 14 || policy == 16) { if (whiteList == null) { whiteList = new ArrayList(); } if (policy == 15 || policy == 14 || policy == 16) { whiteList.add("com.android.deskclock"); } fgTaskPackageMap = WindowProcessUtils.getPerceptibleRecentAppList(this.mActivityManagerService.mActivityTaskManager); } } if (config.isRemoveTaskNeeded() && config.getRemovingTaskIdList() != null) { removeTasksIfNeeded(config.getRemovingTaskIdList(), fgTaskPackageMap != null ? fgTaskPackageMap.keySet() : null, whiteList); } else if (policy == 2) { removeAllTasks(UserHandle.getCallingUserId(), policy); } if (whiteList != null) { if (fgTaskPackageMap != null) { whiteList.addAll(fgTaskPackageMap.values()); } this.mProcessPolicy.addWhiteList(8, whiteList, false); } killAll(policy, reason); return true; }
private void killAll(int policy, String reason) { ArrayList<ProcessRecord> processList; if (this.mLruProcesses != null) { synchronized (this.mActivityManagerService) { try { ActivityManagerService.boostPriorityForLockedSection(); processList = (ArrayList) this.mLruProcesses.clone(); } catch (Throwable th) { ActivityManagerService.resetPriorityAfterLockedSection(); throw th; } } ActivityManagerService.resetPriorityAfterLockedSection(); if (!(processList == null || processList.isEmpty())) { Slog.i("ProcessManager", "mLruProcesses size=" + this.mLruProcesses.size()); filterCurrentProcess(processList, policy); for (int i = processList.size() + (-1); i >= 0; i--) { killOnceByPolicy(processList.get(i), reason, policy); } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| protected boolean swipeToKillApp(ProcessConfig config) throws RemoteException { boolean processHasOtherTask; if (config.isUserIdInvalid() || config.isTaskIdInvalid()) { String msg = "userId:" + config.getUserId() + " or taskId:" + config.getTaskId() + " is invalid"; Slog.w("ProcessManager", msg); throw new IllegalArgumentException(msg); } String packageName = config.getKillingPackage(); if (TextUtils.isEmpty(packageName)) { return false; } int taskId = config.getTaskId(); List<ProcessRecord> appList = getProcessRecordList(packageName, config.getUserId()); boolean appHasOtherTask = isAppHasActivityInOtherTask(appList, taskId); ProcessRecord taskTopApp = null; if (appHasOtherTask) { taskTopApp = ProcessUtils.getTaskTopApp(taskId, this.mActivityManagerService); } if (config.isRemoveTaskNeeded()) { if (appList.isEmpty()) { removeTaskIfNeeded(taskId, packageName, -1); } else { removeTaskIfNeeded(taskId, packageName, appList.get(0).info.uid); } } String killReason = getKillReason(config); if (!appHasOtherTask) { for (ProcessRecord app : appList) { if (!Build.IS_INTERNATIONAL_BUILD || !isAppHasForegroundServices(app)) { killOnceByPolicy(app, killReason, config.getPolicy()); } } return true; } else if (taskTopApp == null) { return true; } else { synchronized (this.mActivityManagerService) { try { ActivityManagerService.boostPriorityForLockedSection(); processHasOtherTask = WindowProcessUtils.isProcessHasActivityInOtherTaskLocked(taskTopApp.getWindowProcessController(), taskId); } catch (Throwable th) { ActivityManagerService.resetPriorityAfterLockedSection(); throw th; } } ActivityManagerService.resetPriorityAfterLockedSection(); if (processHasOtherTask) { return true; } if (Build.IS_INTERNATIONAL_BUILD && isAppHasForegroundServices(taskTopApp)) { return true; } killOnceByPolicy(taskTopApp, killReason, config.getPolicy(), false); return true; } }
|
killOnceByPolicy()
有两个重载,它们最终都会调用到 killOnce()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private void killOnceByPolicy(ProcessRecord app, String reason, int policy) { killOnceByPolicy(app, reason, policy, true); }
private void killOnceByPolicy(ProcessRecord app, String reason, int policy, boolean canForceStop) { if (app != null && app.thread != null && !app.killed && !skipCurrentProcessInBackup(app, app.info.packageName, UserHandle.getCallingUserId())) { int killLevel = 100; if (app.isPersistent() || app.setAdj < 0 || isInWhiteList(app, app.userId, policy)) { ProcessRecordInjector.addAppPssIfNeeded(this, app); if (!app.hasOverlayUi() && !app.hasTopUi() && isTrimMemoryEnable(app.info.packageName)) { killLevel = 101; } } else { killLevel = (!isForceStopEnable(app, policy) || !canForceStop) ? policy == 3 ? 102 : 103 : HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION; } killOnce(app, reason, killLevel); } }
|
继续深入分析,这里 killOnce()
方法引入了一个参数 killLevel,下方还有一个 killLevelToString()
,在这里我们可以了解到各个 killLevel 代表的含义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private String killLevelToString(int level) { switch (level) { case 100: return "none"; case 101: return "trim-memory"; case 102: return "kill-background"; case 103: return "kill"; case HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION : return "force-stop"; default: return ""; } }
|
其中有一个 level 是 ProcessConfig.KILL_LEVEL_TRIM_MEMORY
(101),它不会杀死应用进程,只会对其进行一些内存清理,而这正是我们所需要的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private void killOnce(ProcessRecord app, String reason, int killLevel) { killOnce(app, reason, killLevel, false); }
private void killOnce(final ProcessRecord app, String reason, int killLevel, boolean evenForeground) { if (app != null && app.thread != null && !app.killed) { if (app.processName.equals("com.miui.fmservice:remote")) { final Intent intent = new Intent("miui.intent.action.TURN_OFF"); intent.addFlags(AudioFormat.EVRC); this.mHandler.post(new Runnable() { @Override public void run() { ProcessManagerService.this.mContext.sendBroadcastAsUser(intent, new UserHandle(app.userId)); } }); Slog.i("ProcessManager", "don't kill fmservice just send turn off intent"); return; } if (killLevel >= 102) { Slog.i("ProcessManager", reason + ": " + killLevelToString(killLevel) + " " + app.processName + " Adj=" + app.curAdj + " State=" + app.getCurProcState()); } if (killLevel == 101) { this.mProcessKiller.trimMemory(app, evenForeground); } else if (killLevel == 102) { this.mProcessKiller.killBackgroundApplication(app, reason); } else if (killLevel == 103) { this.mProcessKiller.killApplication(app, reason, evenForeground); } else if (killLevel == 104) { this.mProcessKiller.forceStopPackage(app, reason, evenForeground); } } }
|
同时我们也得到了一个非常优秀的 hook 点,只要 hook 这个 killOnce()
方法,并在 beforeHookedMethod 中把 level 修改为 trim-memory,就可以实现应用免杀。
模块开发
光有这些还不够,要想开发一个成熟的模块,必须能让用户很方便地进行各项配置,这就要求我们的模块进程具有与 system_server 通信的能力(不可能让用户每次修改 AppLock 的作用域都手动修改配置文件,然后重启系统),这里我选择劫持 ProcessManagerNative
类的 onTransact
方法,向 client 发送 Binder 进行通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| object AppLockHelper {
val server by lazy { AppLockManagerService() }
private object TransactionHook : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { catch { if (param.args[0] != TRANSACTION_CODE) return if (packageManager.getNameForUid(Binder.getCallingUid()) != BuildConfig.APPLICATION_ID) return (param.args[2] as Parcel).writeStrongBinder(server) param.result = true } } }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| object AppLockHelper {
val client by lazy { MyApplication.processManager?.let { val data = Parcel.obtain() val reply = Parcel.obtain() try { if (it.transact(TRANSACTION_CODE, data, reply, 0)) { return@lazy IAppLockManager.Stub.asInterface(reply.readStrongBinder()) } } finally { data.recycle() reply.recycle() } return@lazy null } }
}
|
完整代码已经开源到 GitHub,欢迎各位小伙伴来给我点 star(手动狗头
展望
等等!好像刚刚错过了什么东西,让我们回到 killOnceByPolicy()
方法,再仔细看一看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private void killOnceByPolicy(ProcessRecord app, String reason, int policy, boolean canForceStop) { if (app != null && app.thread != null && !app.killed && !skipCurrentProcessInBackup(app, app.info.packageName, UserHandle.getCallingUserId())) { int killLevel = 100; if (app.isPersistent() || app.setAdj < 0 || isInWhiteList(app, app.userId, policy)) { ProcessRecordInjector.addAppPssIfNeeded(this, app); if (!app.hasOverlayUi() && !app.hasTopUi() && isTrimMemoryEnable(app.info.packageName)) { killLevel = 101; } } else { killLevel = (!isForceStopEnable(app, policy) || !canForceStop) ? policy == 3 ? 102 : 103 : HdmiCecKeycode.CEC_KEYCODE_SELECT_MEDIA_FUNCTION; } killOnce(app, reason, killLevel); } }
|
我们重点关注第四行这个 if,只要让代码进入这个 if 分支内执行,那么 killLevel 最后不是 100(none)就是 101(trim-memory),应用进程并不会被杀死。在 if 的三个判断条件中,isInWhiteList()
似乎是我们可以控制的东西
而 MIUI 这样的系统显然不会硬编码规则列表,这自然而然就引出了第二个方案:通过某种手段修改规则列表,将我们需要的应用直接添加到白名单中,实现免注入、即时生效的 AppLock 功能。