前言
Android 11发布以来,不断收拢应用的权限,对于普通用户而言更为安全、高效,但是对于开发者来说,想要做一些相关的优化也越来越困难。"包可见性"就是权限收拢的一环,Android11之前我们可以自由获取手机内安装的应用列表,做一些自定义桌面类似的程序,有了“包可见性”的限制后,能够获取到的只有一些系统应用了,其他应用程序的信息都成为隐私数据不再开放。
包可见性
Android 上的软件包可见性过滤 | Android Developers
如果应用以 Android 11(API 级别 30)或更高版本为目标平台,并查询与设备上已安装的其他应用相关的信息,则系统在默认情况下会过滤此信息。此过滤行为意味着您的应用无法检测设备上安装的所有应用,这有助于最大限度地减少您的应用可以访问但在执行其用例时不需要的潜在敏感信息。
简单来讲除了系统应用之外,其他的应用都需要配置可见性,才能进行访问。那既然认为其他应用为隐私数据了,为啥还要放开一个口子来让我们获取应用信息呢?应用不可避免的要与其他应用进行交互,支付、分享这些功能都需要包可见性才能够使用,Google认为对于这些预设好要进行交互的应用是安全的,而将声明写入AndroidManifest文件中也避免了我们通过代码动态添加大量包名来过度获取用户信息的方式。
常规解决办法
一、设置<queries>指定包的可见性
<manifest package="com.example.game">
<queries>
<!-- Specific apps you interact with, eg: -->
<package android:name="com.example.store" />
<package android:name="com.example.service" />
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
</queries>
...
</manifest>
在AndroidManifest.xml文件中添加<queries>标签,通过指定package name、Intent、provider来声明需要进行交互的应用。
需要注意的是,Android14之后可能会开始限制<queries>的包名数量。权限的进一步收紧意味着我们不能再通过大量添加包名的方式来尽量避免受到包可见性的限制。如果想要获取排名top应用的相关信息,静态写入的方式对于人工维护也是不小的成本。
二、QUERY_ALL_PACKAGES权限
在极少数情况下,如果遇到 <queries> 元素无法提供适当的软件包可见性,您还可以使用 QUERY_ALL_PACKAGES 权限。如果您在 Google Play 上发布应用,那么应用使用此权限需要获得批准。
以下列表提供了一些使用案例的示例,其中 QUERY_ALL_PACKAGES权限适合包括:
- 无障碍应用
- 浏览器
- 设备管理应用
- 安全应用
- 防病毒应用程序
这个权限相当于完全放开包可见性权限,大多数情况下只能自己使用,想要上架商店基本是不可能的。
非常规手段
一、Hook系统函数---需要root
如果系统是通过与应用交互来获取可见性权限,那么只需要hook住读取/使用queries标签值的地方,就有可能获取所有的可见性权限
源码阅读
既然<queries>标签是写在AndroidManifest文件中,通常来说解析也是通过PMS来进行的,在framework源码中检索queries可以轻易找到解析的位置
继续探究该类,可以看到解析出来的packageName最终是存到了PackageImpl中的queriesPackages里面。
检索使用queriesPackages的地方,发现核心方法是canQueryPackage ,当目标包名可以交互时返回true,否则返回false AppsFilterBase.java
方法解析
反射和代理 - 能否直接在程序中进行Hook\
通过分析源码不难看出,解析queries的类是在apk安装阶段进行,而判断可见性权限是在PMS中进行的,没有实际与apk程序进行交互。应用能够hook的类是PMS的Binder通信类,PMS没有通过Binder来查询可见性,因此不能直接在程序中hook PMS代理来绕过可见性判断。Xposed - Hook PMS
在root的情况下,Xposed通过替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持,可以达到hook整个系统中所有进程内存里面的对象的目的;
public boolean canQueryPackage(@NonNull AndroidPackage querying, String potentialTarget) {
if (isMyApp(querying) {
return true;
}
...
}
既然可见性判断是在PMS中,那么Hook系统服务就可以达到我们的目的,但是xposed的方式只能在自己的设备上使用,想要让应用能够在安装的设备上收集其他应用信息还需要探索别的方法。
二、动态替换queries标签
利用Google开了权限的口子,每次打包的时候动态替换最新的top包来实现较高的使用覆盖率
processDebugMainManifest 任务是 Android Gradle 插件中的一个默认任务,用于处理 Debug 构建变体的主要清单文件(AndroidManifest.xml)。 该任务的目的是将主要清单文件处理为可供应用程序构建使用的格式。它会执行一系列操作,如替换变体特定的值、合并库清单文件、处理权限等。
Android打包过程中,processDebugMainManifest任务会在build的临时目录生成一个AndroidManifest.xml用于后续打包,可以在这个任务后添加一个Task,修改AndroidManifest.xml文件中的标签,来实现每次打包的时候动态添加修改最新的包名
编写插件
具体流程
添加一个自定义的MyPlugin插件
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
...
}
}
插件会从服务接口拉取最新top100的包名数据
fun sendRequest() {
val client = OkHttpClient()
// 模拟网络请求
val request = Request.Builder().url("https://www.baidu.com").build()
val countDownLatch = CountDownLatch(1)
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("failure")
// 请求失败处理
e.printStackTrace()
countDownLatch.countDown()
}
override fun onResponse(call: Call, response: Response) {
// 请求成功处理
val responseData = response.body?.string()
response.close()
// mock数据
packageList.add("com.voicemaker.android")
countDownLatch.countDown()
}
})
try {
countDownLatch.await(1, TimeUnit.SECONDS)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
找出所有变体打包任务的名称
val type = target.extensions.findByType(AppExtension::class.java)
// 遍历所有的变体 正常为Debug和Release
type?.applicationVariants?.forEach {
if (applicationId != it.applicationId) {
applicationId = it.applicationId
}
variantNames.add(it.name)
}
target.afterEvaluate {
variantNames.forEach {
// 为process${variantName}MainManifest后添加自定义task
addProcessManifestTask(target, it.capitalized())
}
}
为每一个变体打包任务创建一个task AddQueriesTask 添加到processDebug(Relase)MainManifest后
fun addProcessManifestTask(project: Project, name: String) {
val processManifestTask = project.tasks.getByName(
String.format(
"process%sMainManifest",
name
)
) as ManifestProcessorTask
val mainFiles = processManifestTask.outputs.files
// 过滤出所有的xml文件
val manifests = mainFiles.filter {
it.name.endsWith("xml")
}.map {
it
}
processManifestTask.finalizedBy(
// 在processManifestTask之后执行自定义task
project.tasks.create(
"MyProcessManifest$name",
AddQueriesTask::class.java
).apply {
setData(manifests, packageList)
}
)
}
执行AddQueriesTask 时,使用XmlParser解析Manifest文件,在标签中添加Top100包
open class AddQueriesTask : DefaultTask() {
private var mManifests: List<File>? = null
private var mPackageList: List<String>? = null
@TaskAction
fun doTaskAction() {
println("doTaskAction")
mManifests?.forEach {
// 对Manifest文件添加包名
handleManifestFile(it)
}
}
private fun handleManifestFile(manifest: File) {
if (!manifest.exists()) {
return
}
println("parse file:$manifest")
val xmlParser = XmlParser()
val xmlNode = xmlParser.parse(manifest)
((xmlNode["queries"] as NodeList).firstOrNull() as? Node)?.apply {
mPackageList?.forEach {
// 为queries标签下添加package节点
Node(
this,
"package",
linkedMapOf("android:name" to it)
)
}
writeToManifest(manifest, xmlNode)
}
}
...
}
回写到xml文件中
/**
* 写回文件,保存更改
*/
private fun writeToManifest(manifest:File, node:Node) {
val result = XmlUtil.serialize(node)
Files.write(manifest.toPath(), result.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
println("writeFinish")
}
后续正常执行打包流程
插件打包
在gradle中进行如下配置,插件编写完成后执行publish任务即可在本地创建一个插件包
gradlePlugin {
plugins {
register("MyClass") {
group = "com.example.test"
version = "1.0"
id = "com.example.test.myplugin"
implementationClass = "com.example.test.MyPlugin"
}
}
}
publishing {
repositories {
maven(url = "../repository") // 发布到本地根目录下的 repositories 目录下,发布的 groupId, artifactsId, version 共用插件的字符
}
}
在项目级别的gradle中添加插件引用
dependencies {
classpath("com.example.test:MyPlugin:1.0")
}
在app中添加插件即可
plugins {
id("com.example.test.myplugin")
}
测试权限获取情况
class DefaultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
baseContext.packageManager.queryIntentActivities(Intent(Intent.ACTION_MAIN, null).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
}, 0).forEach {
// 查找对应应用并拉起进行测试
if ("com.voicemaker.android" == it.activityInfo.packageName) {
val className = it.activityInfo.name
baseContext.startActivity(Intent().apply {
setClassName("com.voicemaker.android", className)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
}
}
}
在测试应用中没有静态填写标签内容,应用启动后会去查询系统内安装的voicemaker应用信息,如果能查询到就把它拉起来,实际效果如下,可以看出动态添加的包名成功被系统识别到了
源码地址: