Android 隐私合规检查工具套装

2,192 阅读7分钟

之前写过一篇《隐私合规代码排查思路》的文章,但文章没有将方案开源出来,总觉得差了那么点意思,这次打算把几种常规的检测方法都开源出来,给大家一些借鉴思路。

对于一套完整的隐私合规检查来说,动静结合是非常有必要的,静态用于扫描整个应用隐私 api 的调用情况,动态用于在运行时同意隐私弹框之前是否有不合规的调用,以下列出一些常规的检查方案:

思维导图中 ✅ 打钩的部分都已经实现,后面会讲解这些方案适合应用在什么场景,他们之间有哪些优缺点。

以上所有工具的实现,都是基于隐私配置文件 privacy_api.json 来实现的,也即意味着,你只需要维护一份配置文件即可。

一、静态检查

1、基于项目依赖的字节码扫描

扫描工程下的所有依赖,提取依赖 jar 包下的所有 Class 文件,利用 ASM 工具分析 Class 文件下的所有方法的 insn 指令,找出是否有调用隐私 api 的情况,实现代码片段:

// 1、读取隐私 api 配置文件
val apiList: List<ApiNode> = Gson().fromJson(configFile.bufferedReader(), type)

// 2、获取项目所有依赖
val resolvableDeps = project.configurations.getByName(configurationName).incoming
resolvableDeps.artifactView { conf ->
    conf.attributes { attr ->
        attr.attribute(
            AndroidArtifacts.ARTIFACT_TYPE,
            AndroidArtifacts.ArtifactType.CLASSES_JAR.type
        )
    }
}

// 3、ASM 分析 Class 文件
clazz.methods?.forEach {
    it.instructions
        .filterIsInstance(MethodInsnNode::class.java)
        .forEach Continue@{ node ->
            val callClazz = getClassName(node.owner) ?: return@Continue
            val callMethod = node.name
            checkApiCall(callClazz, callMethod, it.name, clazz, apiList)
        }
}

扫描出来的结果示例:

[ 
  "android.location.LocationManager_requestLocationUpdates": [
    {
      "clazz": "androidx/core/location/LocationManagerCompat$Api31Impl",
      "method": "requestLocationUpdates",
      "dep": "androidx.core:core:1.9.0"
    },
    {
      "clazz": "androidx/core/location/LocationManagerCompat",
      "method": "getCurrentLocation",
      "dep": "androidx.core:core:1.9.0"
    }
  ]
]

由于是依赖扫描,也即意味着 app 工程下的代码是无法参与扫描的,该方案适合基于壳工程的组件化方案,一般壳工程只有一个 Application 类,其他业务组件都是以依赖的方式集成进壳工程打包,该方案的优点是,可以根据扫描出来的结果快速找到模块负责人,并完成修改。

集成方案查看 github 的 DepCheck 插件 README 说明

2、基于 apk 的 smali 扫描

网易云音乐曾经发表过一篇基于 smali 扫描的《Android 隐私合规静态检查》文章,思路就是将 apk 解压,提取出 dex 文件,然后使用 baksmali 库将 dex 转成 smali 文件,然后逐行分析 smali 的方法调用情况,扫描出来的结果示例:

[
  "android.location.LocationManager_requestLocationUpdates": [
    {
      "clazz": "public final Landroidx.core.location.LocationManagerCompat;",
      "method": "public static getCurrentLocation(Landroid/location/LocationManager;Ljava/lang/String;Landroidx/core/os/CancellationSignal;Ljava/util/concurrent/Executor;Landroidx/core/util/Consumer;)V"
    },
    {
      "clazz": "public final Landroidx.core.location.LocationManagerCompat;",
      "method": "public static requestLocationUpdates(Landroid/location/LocationManager;Ljava/lang/String;Landroidx/core/location/LocationRequestCompat;Landroidx/core/location/LocationListenerCompat;Landroid/os/Looper;)V"
    }
  ]
]

由于是基于 apk 扫描,可以直接对各业务线的所有 apk 进行扫描,相比较需要集成进项目打包的扫描工具来说,不用每条业务线都去集成插件,扫描效率比较高。并且,该工具非常适合非开发人员使用,例如测试版本回归时,对最终产物 apk 进行扫描,以此来确定当前版本是否有不合规的调用。当然,基于 apk 的扫描也有缺点,无法像依赖检查那样快速定位到该类是哪个模块的,也即无法快速找到模块负责人。

该方案的实现使用的是 Java Console Application 工程开发的 CLI 工具,可以直接执行命令行来分析结果,只需要提供 apk 路径与隐私 api 配置文件即可(但需要本地 Java 环境),例如:

./ApkCheck /xx/xx/xx.apk /xxx/xx/api.json

具体使用文档查看 github 的 ApkCheck 的 README 说明。

3、Lint 检查

Lint 检查的主要作用是在开发阶段就遏制住隐私 api 的乱调情况,提前暴露问题,实现代码片段:

// 1、读取工程根目录的隐私配置文件
open class BaseDetector : Detector() {
    override fun beforeCheckFile(context: Context) {
        super.beforeCheckFile(context)
        val apiJson = File(context.project.dir.parentFile,API_JSON)
        apiNode = Gson().fromJson(apiJson.bufferedReader(), type)
    }
}

// 2、检查方法调用是否涉及隐私 api 
private class ApiCallUastHandler(val context: JavaContext?) : UElementHandler() {

        override fun visitCallExpression(node: UCallExpression) {
            if (node.isMethodCall()) {
                apiNode.find {
                    context?.evaluator?.isMemberInClass(node.resolve(), it.clazz) == true
                            && it.method.find { m -> m == node.methodName } != null
                }?.let {
                    context?.report(
                        ISSUE,
                        node,
                        context.getLocation(node),
                        REPORT_MESSAGE
                    )
                }
            }
        }
    }

检查效果如下:

image.png

输出的报告:

image.png

具体集成方案查看 github 的 LintCheck 的 README 说明

二、动态检查

在上面的思维导图中,动态检查 Xposed 与 transform 插桩我是没有实现的,因为我发现这两个方案的 ROI 非常低,并且后期难以维护:

  1. 对于 Xposed 方案来说,需要搭配系统 root,对开发与测试都非常不友好,测试环境过于狭窄,即使是基于非 root 的 VirtualXposed ,系统版本兼容性又存在很大的问题,官方 README 描述仅支持 5.0 ~ 10.0 系统,测试环境依然过于狭窄。并且,对于一心只想解决隐私 api 调用情况的 UI 仔来说,Xposed 方案有点过重
  2. 对于 transfrom 插桩来说,这完全就不是一个可行方案,如果你在 transform 阶段做静态扫描,那完全可以通过依赖扫描来解决。如果你想做运行时 hook 替换,你就得解决 invoke-static 与 invoke-virtual 的替换,这两个指令的处理还不一样,并且,你说你要替换,那你替换成啥呢,你的 utils 工具类?那你就要写很多的模版代码,那未来隐私 api 再增加呢,再去写一遍模版代码吗?这后期维护也太难了。

动态检查的唯一解只有运行时 AOP Hook。

1、基于运行时的 AOP hook 框架

在之前文章 《隐私合规代码排查思路》中介绍过使用 epic 来实现 AOP hook,但 epic 仅支持 Android 5.0 ~ 11,对于手持 12 系统的我来说,非常不方便,故而重新搜了下类 epic 的框架。 你还别说,还真找着了,那就是 Pine,支持 Android 4.4(只支持ART) ~ 14 且使用 thumb-2/arm64 指令集的设备,用法与 epic 相近,如下是一个简单的 AOP Hook 操作:

 Pine.hook(Method, object : MethodHook() {
            override fun beforeCall(callFrame: Pine.CallFrame) {
                addStackLog(method.declaringClass.name, method.name)
            }

            override fun afterCall(callFrame: Pine.CallFrame) {}
        })

那么,我们的实现思路就可以读取隐私合规 api 配置文件,然后调用 Pine.hook 即可。运行时效果如下:

image.png

该方案优点是对 Android 系统版本兼容覆盖比较全,可以在不改变原有业务代码的情况下实现 AOP Hook,缺点就是只能针对自己应用进行 Hook,并且只能 Hook Java Method。

具体集成方案查看 github 的 RuntimeCheck 的 README 说明。

题外话:

2、基于 frida 的免 root 方案

基于 Frida 的方案,我最先接触的是 camille,但该方案需要 root,它可以无侵入的实现所有应用的监测,但从 README 与 issue 来看,问题不少。 在搜索同类工具时,有很多采用 frida-server 的方式,需要通过 adb 将 frida-server push 到手机内,然后启动该服务,听着就头皮发麻。 后面搜到 frida gadget 方案,可以直接配置 js 脚本来实现 hook,无需 frida-server:

image.png

大体实现步骤:

  1. 下载 android arm 架构的 frida-gadget.so, 由于 Release 产物比较多,需要点击 Assets 展开更多
  2. 创建 script.js 脚本文件,实现隐私 api 的 hook
  3. frida-gadget.so 与 script.js 写入到本地
  4. 创建 frida-gadget.config.so 文件,内容结构的 path 指向 script.js 在本地的路径
  5. 动态加载 frida-gadget.so 文件,该 so 会读取 frida-gadget.config.so 中的 path 路径,获取到 script.js 文件,并执行该 js 脚本

运行效果如下:

image.png

该方案的优点不需要 root,并且机型适配比较好,frida 还支持 java/native 的 hook,缺点是,该方案只能针对自己应用进行 Hook。

具体集成方案查看 github FridaCheck 的 README 说明。

总结:

对于上述的几个方案,我还是比较喜欢基于静态方案的 apk smali 扫描与基于动态方案的 frida 无侵入式 camille 方案,这两个方法都无需侵入项目即可实现隐私扫描,适合非开发人员使用。

参考链接: