对MIUI13冻结机制-MILLET的分析
1.起因
上一段时间,我对天天在后台耗电的QQ动起了心思。首先我是想着通过QQ的开源框架来登录QQ并通过钉钉钉webhook机器人来及时推送消息。但用了一段时间后,觉得太过于麻烦,又看到有人弄了支持mipush的webhook消息推送,几乎做到了无后台推送,我也想着转到mipsuh,但mipush个人接入是没有办法的,放弃了。最后,我看到QQ接入了华为的推送,遂伪装机型来使用推送。实际上挺及时的,但一来消息就拉起QQ那魔鬼般的耗电进程,我就搞了个xposed模块,只接收消息,不拉起QQ。用久了发现啊,我只要一熄屏,过一会消息就不会立马发送过来,甚至不发送,直到我打开手机屏,那消息就立马弹出来,我以为是QQ在搞魔法,但分析QQ的代码确没有任何结果。直到最近几天看到了火起来的“墓碑”模块,通过cgroup freezer v1或v2来冻结进程,我想着可不可能是哪个进程被冻结了?
2.cgroup以及MIllet
2.1 概述
Millet实质上是通过cgroup对进程进行冻结,其中,framework提供服务给电池和性能,电池和性能处理各种事件,并发出冻结请求来冻结进程以达到省电的效果。下面是分析。
2.2 MIUI中cgroup的挂载情况。
apollo的MIUI13挂载的cgroup,关于cgroup v1的资料可以参考
简单来说就是通过创建文件夹定义一些逻辑组,通过配置文件设置对应的控制器,通过write写入进程号到特殊文件来把一个进程分到一个组,把进程分到这个组后就会收到组所定义的限制。
# 看下挂载了哪些cgroup
mount | grep cgroup
none on /dev/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
none on /dev/cg2_bpf type cgroup2 (rw,nosuid,nodev,noexec,relatime)
none on /dev/cpuctl type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
none on /dev/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent)
none on /dev/stune type cgroup (rw,nosuid,nodev,noexec,relatime,schedtune)
none on /sys/fs/cgroup type tmpfs (rw,seclabel,relatime,mode=750,gid=1000)
none on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)
看一下目录结构
# 冻结的关键freezer,看看有哪些group
cd /sys/fs/cgroup/freezer
find .
.
./cgroup.procs
./cgroup.sane_behavior
./perf
./perf/cgroup.procs
./perf/thawed
./perf/thawed/cgroup.procs
./perf/thawed/freezer.self_freezing
./perf/thawed/tasks
./perf/thawed/freezer.parent_freezing
./perf/thawed/freezer.state
./perf/thawed/notify_on_release
./perf/thawed/cgroup.clone_children
./perf/freezer.self_freezing
./perf/tasks
./perf/freezer.parent_freezing
./perf/freezer.state
./perf/frozen
./perf/frozen/cgroup.procs
./perf/frozen/freezer.self_freezing
./perf/frozen/tasks
./perf/frozen/freezer.parent_freezing
./perf/frozen/freezer.state
./perf/frozen/notify_on_release
./perf/frozen/cgroup.clone_children
./perf/notify_on_release
./perf/cgroup.clone_children
./tasks
./notify_on_release
./release_agent
./cgroup.clone_children
cat ./perf/frozen/freezer.stat
FROZEN
cat ./perf/thawed/freezer.state
THAWED
大体来看,perf下面定义了俩个组,一个是frozen一个是thawed,对应冻结和解冻。只要把进程划分到对应组就能实现冻结和解冻。这也是所谓墓碑机制的核心。
2.3 Millet的发现
实际上让我发现millet的是这一串警告。
起初我以为是MIUI的bug,这警告意味着cgroup的冻结会完全不可用。其实只是因为cgroup挂载时间太晚了。
我搜了下system目录,这个init.milletmonitor.rc很明显就是挂载cgroup的rc
看看rc的内容
apollo:/system/etc # cat ./init/init.milletmonitor.rc
on property:sys.boot_completed=1
#cgroup v1 freezer sys/fs/cgroup entries
mount tmpfs none /sys/fs/cgroup mode=0750,uid=0,gid=1000
mkdir /sys/fs/cgroup/freezer 0750 root system
mount cgroup none /sys/fs/cgroup/freezer freezer
# Xiaomi: Create cgroup in freezer
mkdir /sys/fs/cgroup/freezer/perf 0750 root system
mkdir /sys/fs/cgroup/freezer/perf/frozen 0750 root system
write /sys/fs/cgroup/freezer/perf/frozen/freezer.state FROZEN
chown root system /sys/fs/cgroup/freezer/perf/frozen/cgroup.procs
chmod 0660 /sys/fs/cgroup/freezer/perf/frozen/cgroup.procs
chown root system /sys/fs/cgroup/freezer/perf/frozen/tasks
chmod 0660 /sys/fs/cgroup/freezer/perf/frozen/tasks
mkdir /sys/fs/cgroup/freezer/perf/thawed 0750 root system
chown root system /sys/fs/cgroup/freezer/perf/thawed/cgroup.procs
chmod 0660 /sys/fs/cgroup/freezer/perf/thawed/cgroup.procs
chown root system /sys/fs/cgroup/freezer/perf/thawed/tasks
chmod 0660 /sys/fs/cgroup/freezer/perf/thawed/tasks
start millet_sig
start millet_binder
start millet_pkg
mkdir /sys/fs/cgroup/frozen/
chown system system /sys/fs/cgroup/frozen/cgroup.procs
chown system system /sys/fs/cgroup/frozen/cgroup.freeze
write /sys/fs/cgroup/frozen/cgroup.freeze 1
mkdir /sys/fs/cgroup/unfrozen/
chown system system /sys/fs/cgroup/unfrozen/cgroup.procs
chown system system /sys/fs/cgroup/unfrozen/cgroup.freeze
这个挂载时机是 boot_completed,实际上这时framework已经初始化完成了,所以那个报错太正常了。
着手分析,直接把framework拉到jadx里看看(总不会框架都要混淆吧)
getFrozenPids就是那个报错的方法了。
往下看看有啥其他方法发现。很明显,freezePid就是向cgroup写入进程的pid(v1的方法)
嗯,这也是个很好的hook点,于是经过一番frida 的hook
Java.perform(function () {
let FreezeUtils = Java.use("com.miui.server.greeze.FreezeUtils");
FreezeUtils.DEBUG.value = true
// FreezeUtils.getFrozenPids.implementation = function () {
// console.log('getFrozenPids is called');
// let ret = this.getFrozenPids();
// console.log('getFrozenPids ret value is ' + ret);
// return ret;
// };
function printstack() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
// FreezeUtils.freezePid(8524)
FreezeUtils.freezePid.overload('int', 'int').implementation = function (pid, uid) {
console.log(`freezePid(${pid},${uid})`);
let ret = this.freezePid(pid, uid);
console.log('freezePid ret value is ' + ret);
return ret;
};
FreezeUtils.freezePid.overload('int').implementation = function (pid) {
console.log(`freezePid(${pid})`);
printstack();
let ret = this.freezePid(pid);
console.log('freezePid ret value is ' + ret);
return ret;
};
let Stub = Java.use("miui.greeze.IGreezeManager$Stub");
Stub.onTransact.implementation = function(code, data, reply, flags){
console.log('get Ibinder!');
let ret = this.onTransact(code, data, reply, flags);
// 发现是服务时直接getCallingUid和Pid拿到pid和uid
console.log("caller_uid:"+this.getCallingUid()+"pid"+this.getCallingPid())
// console.log('onTransact ret value is ' + ret);
return ret;
};
// let GreezeManagerService = Java.use("com.miui.server.greeze.GreezeManagerService");
// GreezeManagerService.freezeUids.implementation = function (uids, timeout, fromWho, reason, checkAudioGps) {
// console.log('freezeUids is called');
// let ret = this.freezeUids(uids, timeout, fromWho, reason, checkAudioGps);
// console.log('freezeUids ret value is ' + ret);
// return ret;
// };
});
这是典型的服务调用,直接找出了调用栈,通过iBinder并找出了谁在调用。
freezePid(11516)
java.lang.Throwable
at com.miui.server.greeze.FreezeUtils.freezePid(Native Method)
at com.miui.server.greeze.GreezeManagerService.freezeProcess(GreezeManagerService.java:594)
at com.miui.server.greeze.GreezeManagerService.freezeUids(GreezeManagerService.java:705)
at miui.greeze.IGreezeManager$Stub.onTransact(IGreezeManager.java:244)
at miui.greeze.IGreezeManager$Stub.onTransact(Native Method)
at android.os.Binder.execTransactInternal(Binder.java:1182)
at android.os.Binder.execTransact(Binder.java:1146)
freezePid ret value is true
caller_uid:1000 pid: 5393
这个是谁呢,当之无愧的电池和性能(com.miui.poerkeeper)
着手下一步hook,目标电池与性能
Java.perform(function () {
function printstack() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
let FreezeBinder = Java.use("com.miui.powerkeeper.millet.FreezeBinder");
FreezeBinder.freezeUids.implementation = function (a, b, c, d) {
console.log('freezeUids is called');
printstack()
let ret = this.freezeUids(a, b, c, d);
// console.log('freezeUids ret value is ' + ret);
return ret;
};
// let MilletHandler = Java.use("com.miui.powerkeeper.millet.MilletHandler");
// MilletHandler.handleMessage.implementation = function(message){
// console.log('handleMessage is called');
// console.log('message.what='+message.what)
// let ret = this.handleMessage(message);
// // console.log('handleMessage ret value is ' + ret);
// return ret;
// };
// let MilletPolicy = Java.use("com.miui.powerkeeper.millet.MilletPolicy");
// let MilletUidObserver = Java.use("com.miui.powerkeeper.millet.MilletUidObserver");
// MilletPolicy.isAllowFreeze.implementation = function(i2){
// // console.log('isAllowFreeze is called');
// let ret = this.isAllowFreeze(i2);
// let PkgName = MilletUidObserver.getPkgNameByUid(i2)
// console.log(PkgName +": "+ ret);
// if(PkgName == 'com.huawei.hwid'){
// console.log("不冻结")
// return false;
// }
// // console.log(PkgName)
// return ret;
// };
// MilletUidObserver.getPkgNameByUid.implementation = function(i2){
// console.log('getPkgNameByUid is called');
// let ret = this.getPkgNameByUid(i2);
// console.log('getPkgNameByUid ret value is ' + ret);
// return ret;
// };
});
继续找出调用服务冻结的方法
java.lang.Throwable
at com.miui.powerkeeper.millet.FreezeBinder.freezeUids(Native Method)
at com.miui.powerkeeper.millet.MilletHandler.frozen(Unknown Source:109)
at com.miui.powerkeeper.millet.MilletHandler.handleMessage(Unknown Source:134)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:210)
at android.os.Looper.loop(Looper.java:299)
at android.os.HandlerThread.run(HandlerThread.java:67)
嗯,就是这个方法了。
下面这个方法负责冻结
com.miui.powerkeeper.millet.MilletHandler.frozen
下面这个方法则负责接收一些事件,比如熄屏等。
com.miui.powerkeeper.millet.MilletHandler.handleMessage
3. 给华为推送添加一个白名单
上述冻结的效果是什么呢,就是没有在白名单的进程在锁屏后会被冻结。华为推送因此也会被冻结(就算在设置里关闭所有的限制)。
com.miui.powerkeeper.millet.MilletHandler.frozen 方法负责冻结,并且会调用com.miui.powerkeeper.millet.MilletPolicy.isAllowFreeze(uid)来决定是否冻结
思路很简单,
Hook com.miui.powerkeeper.millet.MilletPolicy.isAllowFreeze,如果uid是华为推送的uid就直接返回false。
借用com.miui.powerkeeper.millet.MilletUidObserver来通过uid获取包名,然后判断即可
Java.perform(function () {
let MilletPolicy = Java.use("com.miui.powerkeeper.millet.MilletPolicy");
let MilletUidObserver = Java.use("com.miui.powerkeeper.millet.MilletUidObserver");
MilletPolicy.isAllowFreeze.implementation = function(i2){
// console.log('isAllowFreeze is called');
let ret = this.isAllowFreeze(i2);
let PkgName = MilletUidObserver.getPkgNameByUid(i2)
console.log(PkgName +": "+ ret);
if(PkgName == 'com.huawei.hwid'){
console.log("不冻结")
return false;
}
// console.log(PkgName)
return ret;
};
});
4.hook的效果
之前,熄灭屏幕就被冻结的华为推送
hook之后,那个男人他没被冻结了!!我能及时收到QQ消息啦!!
5.总结
小米的确是做了很多比较好的设计的,比如这个millet机制,但用户缺少对其完全对控制权力。而且这个millet机制完全可以让app一到后台就被冻结的,但miui没有这么做,而且MIUI既然存在了这样一个机制了,安卓原生的暂停执行已缓存的进程开关还有必要吗?或者那个开关还有用?。最近的兴起的墓碑模块,本质上就是暂停不在前台的进程的执行,要么通过kill发送signal,要么是自己控制cgroup冻结的进程,但如果用在MIUI上的话,可能会因为和系统的冻结产生冲突而造成性能异常。