安装即启动?探索流氓App的自启动“黑科技” (Android系统内鬼之ContentProvider篇)

前段时间发现了一个神奇的app,它居然可以在安装之后立即自启动:

在这里插入图片描述

看到没有,在提示安装成功大概1到2秒后,就直接弹出Toast和通知了! 好神奇啊,在没有第三方app帮忙唤醒的前提下,它是怎么做到首次安装即自启动的呢?


初步分析

难道它监听了应用安装的广播,在收到广播之后立即启动后台服务?
用jadx打开一看,确实有监听应用安装和卸载的BroadcastReceiver:

请添加图片描述

但是从截图上来看,这个receiver只有2个常见的属性: enableexported,甚至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.blockednumberpackage:/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:

在这里插入图片描述

看到没? sharedUserIdprocess都跟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?) = nulloverride fun getType(uri: Uri?) = nulloverride fun insert(uri: Uri?, values: ContentValues?) = nulloverride fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?) = 0override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
}

然后在AndroidManifest里声明一下,并加上"android.content.ContactDirectory"属性:

<providerandroid:name=".AutoStartProvider"android:authorities="AutoStartProvider"android:exported="true"><meta-dataandroid: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到测试机上安装看看:

在这里插入图片描述

哈哈哈哈哈,成功了!居然真的就这么简单!

好了,最后我们来总结一下叭:


总结

  1. 我们发现了一个"神奇"的app之后,准备搞清楚它的原理;

  2. 首先是进行了初步的猜测: 是否监听了自身的安装广播。但在动手验证之后发现并不是;

  3. 接着通过debug法,发现原来是com.android.providers.blockednumber进程调用了getContentProvider获取com.fg.account.kp.provider的实例时,从而间接启动了进程;

  4. 当我们准备debug com.android.providers.blockednumber时却发现在running app list没有这个进程;

  5. 经查看它apk的AndroidManifest.xml文件发现原来是进程名改为android.process.acore了;

  6. 但当我们试图进一步查看反编译之后的class代码时,居然没有找到先前debug时调用堆栈的那些类;

  7. 后面发现原来有好几个跟它声明了相同sharedUserIdprocess的其他app;

  8. 经过分析正确app的代码发现,原来只需要在provider的meta-data里面设置"android.content.ContactDirectory"的属性值为true即可;

  9. 最后我们自己动手写了demo并验证通过。

(以上内容仅供学习交流,不要用来干坏事噢~)

文章到此结束,有错误的地方请指出,谢谢大家!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/783303.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++2D原创我的世界1.00.3版本上市!!!

我很郁闷&#xff0c;为什么就是整不了昼夜交替啊喂&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 虽然这看上去很简单&#xff0c;但做起来要我命&#xff01;&#xff01;&#xff01; 优化过后总共1312行&#xff0c…

DOM 节点遍历:掌握遍历 XML文档结构和内容的技巧

遍历是指通过或遍历节点树 遍历节点树 通常&#xff0c;您想要循环一个 XML 文档&#xff0c;例如&#xff1a;当您想要提取每个元素的值时。 这被称为"遍历节点树"。 下面的示例循环遍历所有 <book> 的子节点&#xff0c;并显示它们的名称和值&#xff1a;…

Qt主窗口 之:停靠/悬浮窗口(QDockWidget)

一、QDockWidget概述 QDockWidget 是 Qt 中的一个窗口部件&#xff0c;用于创建可停靠的窗口&#xff0c;通常用于构建多文档接口&#xff08;MDI&#xff09;或可定制的用户界面。QDockWidget 允许用户将窗口停靠在应用程序的主窗口周围&#xff0c;或将其拖动到独立的浮动窗…

门控循环单元(GRU)

概述 门控循环单元&#xff08;Gated Recurrent Unit, GRU&#xff09;由Junyoung Chung等人于2014年提出&#xff0c;原论文为《Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling》。GRU是循环神经网络&#xff08;Recurrent Neural Network, …

实现富文本的三部曲

1、引入 ueditor.config.js ueditor.all.min.js lang/zh-cn/zh-cn.js 2、编辑器显示处 id"content" <textarea id"content" name"content"></textarea> 3、底部 <script type"text/javascript"> //实例化编辑器 …

2024.3.30学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p295-p314 super关键字 super代表父类的引用&#xff0c;用于访问父类的属性、方法、构造器 super细节和语法 访问父类的属性&#xff0c;但不能访问父类的private属性 super.属性名 访问父类的…

回溯算法|39.组合总和

力扣题目链接 class Solution { private:vector<vector<int>> result;vector<int> path;void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {if (sum > target) {return;}if (sum target) {result.push_back…

Flutter(踩坑)之Android sdkmanager tool not found

D:\Flutter\flutter\bin\flutter.bat doctor --verbose [√] Flutter (Channel stable, v1.2.1, on Microsoft Windows [Version 10.0.22631.3296], locale zh-CN)• Flutter version 1.2.1 at D:\Flutter\flutter• Framework revision 8661d8aecd (5 years ago), 2019-02-14 …

【C++航海王:追寻罗杰的编程之路】priority_queue(优先队列) | 容器适配器你知道哪些?

目录 1 -> priority_queue的介绍和使用 1.1 -> priority_queue的介绍 1.2 -> priority_queue的使用 1.3 -> priority_queue的模拟实现 2 -> 容器适配器 2.1 -> 什么是适配器 2.2 -> STL标准库中stack和queue的底层结构 2.3 -> deque的介绍 2.…

Flutter 全局控制底部导航栏和自定义导航栏的方法

1. 介绍 导航栏在移动应用中扮演着至关重要的角色&#xff0c;它是用户与应用之间进行导航和交互的核心组件之一。无论是简单的页面切换&#xff0c;还是复杂的应用导航&#xff0c;导航栏都能够帮助用户快速找到所需内容&#xff0c;提升用户体验和应用的易用性。 在移动应用…

Mamba: Linear-Time Sequence Modeling with Selective State Spaces(论文笔记)

What can I say? 2024年我还能说什么&#xff1f; Mamba out! 曼巴出来了&#xff01; 原文链接&#xff1a; [2312.00752] Mamba: Linear-Time Sequence Modeling with Selective State Spaces (arxiv.org) 原文笔记&#xff1a; What&#xff1a; Mamba: Linear-Time …

进程等待+替换

✨MyShell实现✨ c并发编程&#xff08;书籍&#xff09; ✨进程等待  ✨wait/waitpid   ✨代码示例  ✨coredump   ✨什么是coredump   ✨开启coredump功能   ✨示例代码   ✨退出码 ✨进程替换  ✨原理  ✨进程替换接口一览  ✨实现一个shell  ✨myshell反思…

单链表就地逆置

算法思想&#xff1a;构建一个带头结点的单链表L&#xff0c;然后访问链表中的每一个数据结点&#xff0c;将访问到的数据结点依此插入到L的头节点之后。 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> typedef int ElemType; typedef s…

【学习】软件科技成果鉴定测试有何作用

软件科技成果鉴定测试是针对软件进行项目申报、科技成果鉴定等相关目的进行的测试。软件测试报告可作为项目申报、科技成果鉴定等工作的依据之一。软件类科技成果鉴定测试从软件文档、功能性、使用技术等方面对软件系统进行符合性测试。其测试结果证明软件的质量是否符合技术合…

智能指针(C++11)

智能指针的使用 问题 我们在平时写程序的时候&#xff0c;有些情况下不可避免地会遇见内存泄露的情况。内存泄露是指因为疏忽或错误&#xff0c;造成程序未能释放已经不再使用的内存的情况。例如下面这个例子&#xff0c;内存泄漏不易被察觉。 int div() {int a, b;cin >…

Vue tree自定义滚动条位置

贴一张效果图&#xff0c;我的效果不方便贴出来 实现支持&#xff1a; 1、懒加载 2、普通加载 下面贴关键思想&#xff1a; document有一个获取element元素的方法。 let element document.getElementById(tree); let arr document.querySelectorAll(".nodelModel&quo…

【JDK常用的API】包装类

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

【IDEA】安装教程

目录 一、安装IDEA 二、激活IDEA 总结 一、安装IDEA 点击idea安装包->点击next->选择安装的路径->勾选创建桌面的快捷方式->勾选将bin目录添加到环境变量->勾选文件夹当做项目工程 打开下面这四个不勾选&#xff0c;勾选表示打开文件将以下面的格式打开 安装…

Python绘制线图之plt.plot()的介绍以及使用

在Python中plt.plot是matplotlib库中的一个函数,用于绘制点和线,并对其样式进行控制,下面这篇文章主要给大家介绍了关于Python绘制线图之plt.plot()的介绍以及使用的相关资料,需要的朋友可以参考下 plt.plot() 是Matplotlib库中用于绘制线图&#xff08;折线图&#xff09;的主…

YOLOv9改进策略 :主干篇 | 南开大学提出LSKNet,遥感旋转目标检测新SOTA ,ICCV 2023

💡💡💡本文改进内容: 动态调整特征提取骨干的感受野,以便更有效地处理被检测大小物体的不同的检测能力,也就是说可以有效提升检测数据集当中存在大小目标的检测能力 改进结构图如下: 《YOLOv9魔术师专栏》将从以下各个方向进行创新: 【原创自研模块】【多组合点优…