某音乐软件在原生Pixel被拦截自启动后导致系统NFC无限崩溃
本文代码基于Android 12
起因
在调试Pixel的时候,发现每次重启,国内某音乐软件的播放通知栏就会显示在锁屏上,按照以前的逻辑,这应该是接收开机广播拉起的进程,但调查之后却发现事实并没有那么简单,接下来让我们一起去看看这个软件的自启方式(不知道google issue tracker上面有没有对应的issue,希望google能尽早修复这个问题吧)。
某音乐软件如何自启
一般遇到这些应用自启的问题,先从ProcessList#startProcessLocked
方法入手,因为所有应用进程创建都会经过这里,把AOSP代码导入Android Studio中,在这个方法打一个断点,当命中断点的时候,输出堆栈log,我们来看看输出的堆栈
05-09 01:34:41.891 3174 3442 E ContentPane: startProcess: name=com.xxxxxx.xxmusic app=null knownToBeDead=true thread=null pid=-1
05-09 01:34:41.891 3174 3442 E ContentPane: java.lang.Throwable: ContentPane
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:2492)
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:2686)
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ActiveServices.bringUpServiceLocked(ActiveServices.java:3845)
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ActiveServices.bindServiceLocked(ActiveServices.java:2819)
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ActivityManagerService.bindIsolatedService(ActivityManagerService.java:12003)
05-09 01:34:41.891 3174 3442 E ContentPane: at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2606)
05-09 01:34:41.891 3174 3442 E ContentPane: at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2498)
05-09 01:34:41.891 3174 3442 E ContentPane: at android.os.Binder.execTransactInternal(Binder.java:1179)
05-09 01:34:41.891 3174 3442 E ContentPane: at android.os.Binder.execTransact(Binder.java:1143)
一般接收开机广播起来的话都是从BroadcastQueue
调用过来,但是可以看到这个进程是通过ActiveServices#bindServiceLocked
起来的,也就是应用或者系统调用startService
或bindService
的时候,发现没有相应进程,而调用ProcessList#startProcessLocked
拉起相应组件的进程。
接下来,就是看看是谁拉起的进程,我们接着在ActivityManagerService#bindIsolatedService
方法,通过Binder.getCallingPid
和Binder.getCallingUid
都打印出来。
05-09 01:34:41.691 3174 3442 D ContentPane: service is Intent { act=android.nfc.cardemulation.action.HOST_APDU_SERVICE cmp=com.xxxxxx.xxmusic/com.xxxxxx.xx.plugin.appbrand.jsapi.nfc.hce.HCEService } callingPackage is com.android.nfc uid=1027 pid=4043
发现是nfc通过action拉起的服务,此时的我明白了一些问题,原来是这个音乐软件利用系统服务或者系统应用初始化时或者执行某些流程时需要绑定一些第三方应用的action的特性,来把自己的进程拉起来。
如何通过ifw拦截某音乐软件启动
接下来问题就很好解决了,一切的component或者aciton都可以被IntentFirewall
拦截下来,只需要编写一些简单的规则,就可以禁止该component或者action的启动,不了解IntentFirewall
的同学可以看IFW 是什么,而规则的编写可以在github上搜索规则的关键字component-filter
或者intent-filter
,它们分别代表通过组件过滤启动、通过action过滤启动。而我就编写了一个规则
<rules>
<service block="true" log="true">
<component-filter name="com.xxxxxx.xxmusic/com.xxxxxx.xx.plugin.appbrand.jsapi.nfc.hce.HCEService" />
</service>
</rules>
这个规则的意思就是告诉AMS如果有组件名长这样的,就把它拦截下来不允许启动,把这个写好的规则保存为xml并push到路径data/system/ifw
下,因为IntentFirewall
通过FileObserver
监测这个文件夹文件的这些操作,所以规则是马上生效的。
private static final int MONITORED_EVENTS = FileObserver.CREATE|FileObserver.MOVED_TO|
FileObserver.CLOSE_WRITE|FileObserver.DELETE|FileObserver.MOVED_FROM;
但在这里,我们只有重启才能复现现象,所以我们重启手机,果然该音乐应用没有启动,没有显示在锁屏界面上,ps进程也没发现该音乐应用的进程启动,说明我们的规则是生效了。
拦截某音乐软件启动后手机莫名发热
这时候我就没有继续看该应用的自启问题,转而去看其他应用的自启问题,但过了一会发现手机一直发热,电量也在快速往下掉,我打印event log发现nfc应用一直在crash且重新创建进程,然后我执行adb logcat -s AndroidRuntime
发现这个crash堆栈一直在刷
05-09 05:03:38.341 1619 1619 E AndroidRuntime: java.lang.SecurityException: Not allowed to bind to service Intent { act=android.nfc.cardemulation.action.HOST_APDU_SERVICE cmp=com.xxxxxx.xxmusic/com.xxxxxx.xx.plugin.appbrand.jsapi.nfc.hce.HCEService }
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:1984)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.app.ContextImpl.bindServiceAsUser(ContextImpl.java:1919)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.content.ContextWrapper.bindServiceAsUser(ContextWrapper.java:829)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at com.android.nfc.cardemulation.HostEmulationManager.bindPaymentServiceLocked(HostEmulationManager.java:382)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at com.android.nfc.cardemulation.HostEmulationManager.lambda$onPreferredPaymentServiceChanged$0$HostEmulationManager(HostEmulationManager.java:118)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at com.android.nfc.cardemulation.HostEmulationManager$$ExternalSyntheticLambda0.run(Unknown Source:4)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:938)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:99)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:201)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.os.Looper.loop(Looper.java:288)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7839)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
05-09 05:03:38.341 1619 1619 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(Runt
通过这个堆栈可以看出,因为无法启动这个服务,所以在ContextImpl
中抛出了SecurityException
异常,而且com.android.nfc
本来是一个persist
应用,所以会一直创建进程,一直crash,一直杀进程,这样手机不发热才怪呢。。
frameworks/base/core/java/android/app/ContextImpl.java
private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,
String instanceName, Handler handler, Executor executor, UserHandle user) {
// ...
try {
// ...
int res = ActivityManager.getService().bindIsolatedService(
mMainThread.getApplicationThread(), getActivityToken(), service,
service.resolveTypeIfNeeded(getContentResolver()),
sd, flags, instanceName, getOpPackageName(), user.getIdentifier());
if (res < 0) {
throw new SecurityException(
"Not allowed to bind to service " + service);
}
return res != 0;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
修复Nfc无限崩溃问题
发现这个问题后,我马上删除了ifw的规则文件,但这样又无法管控住这个应用的自启动,我尝试转换思路,只要我在nfc代码中catch住这个SecurityException
异常,那是不是nfc就不会crash了,通过堆栈,我们很快定位到HostEmulationManager.java
这个文件的报错,我们看下报错的代码
376 void bindPaymentServiceLocked(int userId, ComponentName service) {
377 unbindPaymentServiceLocked();
378
379 Intent intent = new Intent(HostApduService.SERVICE_INTERFACE);
380 intent.setComponent(service);
381 mLastBoundPaymentServiceName = service;
382 if (mContext.bindServiceAsUser(intent, mPaymentConnection,
383 Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
384 new UserHandle(userId))) {
385 mPaymentServiceBound = true;
386 } else {
387 Log.e(TAG, "Could not bind (persistent) payment service.");
388 }
389 }
可以看到382行代码,正是我们一直crash的地方,执行着bindService的操作,但没有catch SecurityException
的代码,所以当可以找到组件但无法启动这个service的时候,这里就会发生crash。有趣的是,我在这个类HostEmulationManager.java
发现另外一个方法,这里会对bindServiceAsUser
进行try catch
,但看方法名后缀是IfNeeded
,这在系统源码的一些约定俗成的规则代表可能执行也可能不执行,所以用try catch
进行异常的捕捉。
Messenger bindServiceIfNeededLocked(ComponentName service) {
if (mPaymentServiceName != null && mPaymentServiceName.equals(service)) {
Log.d(TAG, "Service already bound as payment service.");
return mPaymentService;
} else if (mServiceName != null && mServiceName.equals(service)) {
Log.d(TAG, "Service already bound as regular service.");
return mService;
} else {
Log.d(TAG, "Binding to service " + service);
unbindServiceIfNeededLocked();
Intent aidIntent = new Intent(HostApduService.SERVICE_INTERFACE);
aidIntent.setComponent(service);
try {
mServiceBound = mContext.bindServiceAsUser(aidIntent, mConnection,
Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
UserHandle.CURRENT);
if (!mServiceBound) {
Log.e(TAG, "Could not bind service.");
}
} catch (SecurityException e) {
Log.e(TAG, "Could not bind service due to security exception.");
}
return null;
}
}
而我的workaround则是模仿这个方法,对bindServiceAsUser
进行try catch
,但没仔细分析如果这个PaymentService
不绑定的话会对系统nfc有多大的影响,但这个音乐软件没安装之前,也没有对应action的PaymentService
,我猜对nfc影响应该不大。
void bindPaymentServiceLocked(int userId, ComponentName service) {
unbindPaymentServiceLocked();
Intent intent = new Intent(HostApduService.SERVICE_INTERFACE);
intent.setComponent(service);
mLastBoundPaymentServiceName = service;
try {
mPaymentServiceBound = mContext.bindServiceAsUser(intent, mPaymentConnection,
Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
new UserHandle(userId));
if (!mPaymentServiceBound) {
Log.e(TAG, "Could not bind service.");
}
} catch (SecurityException e) {
Log.e(TAG, "Could not bind (persistent) payment service due to security exception.");
}
}
修改后,我重新编译了Nfc的系统App,push到Nfc的扫描路径下、把规则重新push到路径data/system/ifw
后重启,某音乐软件没有自启,且Nfc也不再崩溃了。
某音乐软件如何让自己被Nfc拉起
这里简单分析下音乐软件如何让自己被Nfc拉起,通过dumpsys package,发现以下信息
Service Resolver Table:
Non-Data Actions:
android.nfc.cardemulation.action.HOST_APDU_SERVICE:
13200b3 com.xxxxxx.xxmusic/com.xxxxxx.xx.plugin.appbrand.jsapi.nfc.hce.HCEService filter bf30670 permission android.permission.BIND_NFC_SERVICE
Action: "android.nfc.cardemulation.action.HOST_APDU_SERVICE"
Category: "android.intent.category.DEFAULT"
通过在AndroidManifest.xml
中把自己的服务HCEService
和Action: "android.nfc.cardemulation.action.HOST_APDU_SERVICE"
进行绑定,用户重启手机的时候,nfc进程也会启动,nfc进程启动的时候会寻找android.nfc.cardemulation.action.HOST_APDU_SERVICE
对应的组件进行绑定,这样就给第三方应用进程拉起来了。
说在后面
对于上面这样的情况,如果是普通用户,对这个音乐软件的启动根本毫无办法,就算通过ifw
拦截了该组件,如果不修改nfc应用的代码,nfc进程无限崩溃,电量肉眼可见地掉下来,这也是不可接受的,而nfc系统软件是不可卸载的,因为是persist应用,自然也不能通过更新解决(修改过系统源码的话当我没说),可以说这个自启方法让我见识到了App自启的"决心",和ROM厂商在防止App自启上与App的斗智斗勇,不得不说,这个自启方法,如果不是手上有源码可以编译或者root的情况下(root可以删除nfc),基本就是无解的,国内软件生态还需继续加油。。