前段时间发现了一个神奇的app,它居然可以在安装之后立即自启动:
看到没有,在提示安装成功大概1到2秒后,就直接弹出Toast和通知了! 好神奇啊,在没有第三方app帮忙唤醒的前提下,它是怎么做到首次安装即自启动的呢?
初步分析
难道它监听了应用安装的广播,在收到广播之后立即启动后台服务?
用jadx打开一看,确实有监听应用安装和卸载的BroadcastReceiver:
但是从截图上来看,这个receiver只有2个常见的属性: enable
和exported
,甚至intent-filter
都没有设置优先级,分明就是一个很普通的receiver嘛。
而且按常理,在android系统上,新安装的app如果没有主动运行过一次,那么它所有的BroadcastReceiver都是不会生效的,例如监听应用安装卸载、监听设备开机、熄屏亮屏等。
就算它有办法绕过这个限制,那它真的能接收到自身的安装广播吗?(反正这种操作我是第一次见)
不过我还是仿照它的做法,写demo测试了一下……
得到的结果是: 接收不到任何广播。
这就说明这个app的【安装完自启动】并不是通过监听自身的安装广播来实现的。
那么,它到底是怎么启动的呢,会是谁启动了它呢?
也许我们可以使用debug法来进行分析(当然,debug系统进程需要手机获取root权限,或者直接刷入一个user-debug/eng系统,这不在本文的讨论范围内)。
有同学可能会说,可以在AMS的attachApplication
方法里打断点,因为这是app进程启动的必经之路。
emmmm,这是必经之路没错,但如果在这里打断点已经迟了,因为这时候进程已经启动,依然无法得知是由哪个进程发起的。
所以我们应该尽量在靠近启动源头的地方打断点。
寻找启动源头
先来复习一下常规应用进程的启动流程:
查看大图
可以看到,向zygote发起fork请求的是system_process进程,我们可以在system_process这条线上的任意一个方法打断点,比如ZygoteProcess.start
方法:
等下就可以顺着堆栈去找到启动的源头了。
如果你的手机不是user-debug/eng系统但有root权限(现在获取root权限基本上都是刷magisk了吧?),可以直接在shell中通过以下命令来临时(重启后失效)开启全局debug:
magisk resetprop ro.debuggable 1&&stop;start
好,attach上system_process进程:
现在卸载重新安装一遍(等它自启动):
来了来了,就是这个com.fg
!来看下调用链的前半段(注意选中的那个lambda):
原来这里有个Handler.post
,我们在它外面再打一个断点,这样就能看到post
之前的调用链了:
好,再次卸载重新安装(等它自启动):
咦???为什么源头是AMS的getContentProvider
方法啊?
看下变量面板:
这个callingPackage
就是本次调用getContentProvider
方法的进程包名;
name
即目标ContentProvider在AndroidManifest中声明的authorities(系统唯一);
现在可以得出结论:
app在安装之后,com.android.providers.blockednumber进程会通过getContentProvider
获取com.fg.account.kp.provider而间接启动了进程!
那么,为什么blockednumber进程要获取这个provider呢?
还是继续debug根据堆栈来溯源吧:
咦?奇怪,居然没有com.android.providers.blockednumber进程。
很有可能是它修改了进程名。 我们现在已经知道了它的包名,可以通过pm path
命令来得到对应apk的路径:
:~$ adb shell pm path com.android.providers.blockednumber
package:/system/priv-app/BlockedNumberProvider/BlockedNumberProvider.apk
把它pull上来然后拖进as看下AndroidManifest:
:~$ adb pull /system/priv-app/BlockedNumberProvider/BlockedNumberProvider.apk .
/system/priv-app/BlockedNumberProvider...ed. 12.6 MB/s (303518 bytes in 0.023s)
emmmm,果然没猜错,进程名改为android.process.acore
了,也就是上图中的第二个进程。
赶紧attach上,然后给IActivityManager的getContentProvider
方法打上断点:
再把那个apk继续重安装一遍(等它自启动):
断点到了!把调用链整理一下:
android.app.IActivityManager$Stub$Proxy.getContentProvider() -->
android.app.ActivityThread.acquireProvider() -->
android.content.ContextImpl$ApplicationContentResolver.acquireUnstableProvider() -->
android.content.ContentResolver.acquireUnstableProvider() -->
android.content.ContentResolver.query() -->
com.android.providers.contacts.ContactDirectoryManager.queryDirectoriesForAuthority() -->
com.android.providers.contacts.ContactDirectoryManager.updateDirectoriesForPackage() -->
com.android.providers.contacts.ContactDirectoryManager.onPackageChanged() -->
com.android.providers.contacts.ContactsProvider2.onPackageChanged() -->
com.android.providers.contacts.ContactsPackageMonitor.onPackageChanged() -->
com.android.providers.contacts.ContactsPackageMonitor.onPerformTask() -->
com.android.providers.contacts.ContactsTaskScheduler$MyHandler.handleMessage() -->
android.os.Handler.dispatchMessage() -->
android.os.Looper.loop() -->
android.os.HandlerThread.run()
原来getContentProvider
是因为ContactDirectoryManager.queryDirectoriesForAuthority
里面调用了ContentResolver.query
方法而间接调用到的。
继续往下看,是连续三个onPackageChanged
,根据方法名再结合刚刚安装apk的现象,就很容易能猜到它是监听了应用安装的广播。
好,现在用jadx打开刚刚pull上来的BlockedNumberProvider.apk,看下它这几个类的代码:
咦??为什么没有这些类呢? 甚至都没看到com.android.providers.contacts包名!
再看一眼Manifest:
它居然指定了sharedUserId为android.uid.shared
!这样看来,很可能不止它一个app在用这个sharedUserId。了解过sharedUserId的同学都知道,如果不同的app声明了相同的sharedUserId和相同的进程名,那么这些app就会运行在同一个进程中!
所以我们前面debug时看到的com.android.providers.contacts
这些包名的class,很可能就在另外一个app上。
有什么办法可以查到还有哪些app跟它使用了同样的sharedUserId呢?
很简单,只需要运行adb shell dumpsys package com.android.providers.blockednumber
:
看第二个: com.android.providers.contacts
,这不刚好就是上面调用了ContentResolver.query
方法的包名吗?
用前面的方法把它pull上来用jadx看看吧:
上面调用链里出现的类,在这里都找到了。
再确认一下Manifest:
看到没? sharedUserId
和process
都跟BlockedNumberProvider.apk是一样的,这就证明了这两个apk是运行在同一进程中的。
代码分析
先回顾一下之前断点到的调用链:
android.app.IActivityManager$Stub$Proxy.getContentProvider() -->
android.app.ActivityThread.acquireProvider() -->
android.content.ContextImpl$ApplicationContentResolver.acquireUnstableProvider() -->
android.content.ContentResolver.acquireUnstableProvider() -->
android.content.ContentResolver.query() -->
com.android.providers.contacts.ContactDirectoryManager.queryDirectoriesForAuthority() -->
com.android.providers.contacts.ContactDirectoryManager.updateDirectoriesForPackage() -->
com.android.providers.contacts.ContactDirectoryManager.onPackageChanged() -->
com.android.providers.contacts.ContactsProvider2.onPackageChanged() -->
com.android.providers.contacts.ContactsPackageMonitor.onPackageChanged() -->
com.android.providers.contacts.ContactsPackageMonitor.onPerformTask() -->
com.android.providers.contacts.ContactsTaskScheduler$MyHandler.handleMessage() -->
android.os.Handler.dispatchMessage() -->
android.os.Looper.loop() -->
android.os.HandlerThread.run()
最后是在ContactDirectoryManager的queryDirectoriesForAuthority
方法里调用ContentResolver.query
方法,看下它的代码:
protected void queryDirectoriesForAuthority(ArrayList<DirectoryInfo> arrayList, ProviderInfo providerInfo) {
Cursor cursor = null;
try {
cursor = this.mContext.getContentResolver().query(new Uri.Builder().scheme("content")
.authority(providerInfo.authority).appendPath("directories").build(), DirectoryQuery.PROJECTION, null, null, null);
if (cursor == null) {
......
} else {
while (cursor.moveToNext()) {
DirectoryInfo directoryInfo = new DirectoryInfo();
directoryInfo.packageName = providerInfo.packageName;
directoryInfo.authority = providerInfo.authority;
directoryInfo.accountName = cursor.getString(0);
directoryInfo.accountType = cursor.getString(1);
directoryInfo.displayName = cursor.getString(2);
......
arrayList.add(directoryInfo);
}
}
} catch (Throwable th) {
......
}
}
大致的逻辑就是把查询出来的Provider信息放进一个ArrayList里面。
注意:上面调用getContentResolver().query
的时候,如果要查询的Provider进程不在运行中,AMS会尝试启动这个Provider所在进程!
好,接下来看看在什么情况下它会调用这个queryDirectoriesForAuthority
方法:
private List<DirectoryInfo> updateDirectoriesForPackage(PackageInfo packageInfo, boolean z) {
......
ArrayList<DirectoryInfo> newArrayList = Lists.newArrayList();
ProviderInfo[] providerInfoArr = packageInfo.providers;
if (providerInfoArr != null) {
for (ProviderInfo providerInfo : providerInfoArr) {
// 这里
if (isDirectoryProvider(providerInfo)) {
queryDirectoriesForAuthority(newArrayList, providerInfo);
}
}
}
......
}
原来是通过isDirectoryProvider
方法来判断的,看下它的代码:
static boolean isDirectoryProvider(ProviderInfo providerInfo) {
if (providerInfo == null) return false;
Bundle metaData = providerInfo.metaData;
if (metaData == null) return false;
Object obj = metaData.get("android.content.ContactDirectory");
return obj != null && Boolean.TRUE.equals(obj);
}
它是判断这个provider的metaData
中的"android.content.ContactDirectory"
属性是否为true!
还记得前面debug看到的那个被拉起的provider叫什么吗?
没错就是com.fg.account.kp.provider
,那么现在我们来看下它在AndroidManifest中的声明:
妈耶!!!它meta-data
里的"android.content.ContactDirectory"
属性就是true!
真的只有这么简单吗?只需要在provider里面设置这个meta-data属性为true就可以实现安装自启动?
我们来写个demo来验证下叭!
效果验证
首先写一个ContentProvider,并在onCreate
方法里打印日志:
class AutoStartProvider : ContentProvider() {
override fun onCreate(): Boolean {
Log.e("AutoStartProvider", "process started")
return true
}
override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?) = null
override fun getType(uri: Uri?) = null
override fun insert(uri: Uri?, values: ContentValues?) = null
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
}
然后在AndroidManifest里声明一下,并加上"android.content.ContactDirectory"
属性:
<provider
android:name=".AutoStartProvider"
android:authorities="AutoStartProvider"
android:exported="true">
<meta-data
android:name="android.content.ContactDirectory"
android:value="true" />
</provider>
再加个前台服务,跟随app一起启动:
class AutoStartService : Service() {
override fun onCreate() {
super.onCreate()
setForeground()
Toast.makeText(this, "Service started", Toast.LENGTH_LONG).show()
}
private fun setForeground() {
val channelId = "auto_start"
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH).apply {
setSound(null, null)
setShowBadge(false)
})
startForeground(
1, Notification.Builder(this, channelId)
.setContentTitle("Service started")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.build()
)
}
override fun onBind(intent: Intent?): IBinder? = null
}
好,push到测试机上安装看看:
哈哈哈哈哈,成功了!居然真的就这么简单!
好了,最后我们来总结一下叭:
总结
-
我们发现了一个"神奇"的app之后,准备搞清楚它的原理;
-
首先是进行了初步的猜测: 是否监听了自身的安装广播。但在动手验证之后发现并不是;
-
接着通过debug法,发现原来是
com.android.providers.blockednumber
进程调用了getContentProvider
获取com.fg.account.kp.provider
的实例时,从而间接启动了进程; -
当我们准备debug
com.android.providers.blockednumber
时却发现在running app list没有这个进程; -
经查看它apk的AndroidManifest.xml文件发现原来是进程名改为
android.process.acore
了; -
但当我们试图进一步查看反编译之后的class代码时,居然没有找到先前debug时调用堆栈的那些类;
-
后面发现原来有好几个跟它声明了相同
sharedUserId
和process
的其他app; -
经过分析正确app的代码发现,原来只需要在provider的meta-data里面设置
"android.content.ContactDirectory"
的属性值为true即可; -
最后我们自己动手写了demo并验证通过。
(以上内容仅供学习交流,不要用来干坏事噢~)
文章到此结束,有错误的地方请指出,谢谢大家!