之前写过一篇《隐私合规代码排查思路》的文章,但文章没有将方案开源出来,总觉得差了那么点意思,这次打算把几种常规的检测方法都开源出来,给大家一些借鉴思路。
对于一套完整的隐私合规检查来说,动静结合是非常有必要的,静态用于扫描整个应用隐私 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
)
}
}
}
}
检查效果如下:
输出的报告:
具体集成方案查看 github 的 LintCheck 的 README 说明
二、动态检查
在上面的思维导图中,动态检查 Xposed 与 transform 插桩我是没有实现的,因为我发现这两个方案的 ROI 非常低,并且后期难以维护:
- 对于 Xposed 方案来说,需要搭配系统 root,对开发与测试都非常不友好,测试环境过于狭窄,即使是基于非 root 的 VirtualXposed ,系统版本兼容性又存在很大的问题,官方 README 描述仅支持 5.0 ~ 10.0 系统,测试环境依然过于狭窄。并且,对于一心只想解决隐私 api 调用情况的 UI 仔来说,Xposed 方案有点过重
- 对于 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 即可。运行时效果如下:
该方案优点是对 Android 系统版本兼容覆盖比较全,可以在不改变原有业务代码的情况下实现 AOP Hook,缺点就是只能针对自己应用进行 Hook,并且只能 Hook Java Method。
具体集成方案查看 github 的 RuntimeCheck 的 README 说明。
题外话:
- Pine 的实现思路可以看《ART上的动态Java方法hook框架》,这是一篇 2020 年写的文章,关于信息里面,作者当前年龄 19 岁.....
2、基于 frida 的免 root 方案
基于 Frida 的方案,我最先接触的是 camille,但该方案需要 root,它可以无侵入的实现所有应用的监测,但从 README 与 issue 来看,问题不少。 在搜索同类工具时,有很多采用 frida-server 的方式,需要通过 adb 将 frida-server push 到手机内,然后启动该服务,听着就头皮发麻。 后面搜到 frida gadget 方案,可以直接配置 js 脚本来实现 hook,无需 frida-server:
大体实现步骤:
- 下载 android arm 架构的 frida-gadget.so, 由于 Release 产物比较多,需要点击 Assets 展开更多
- 创建 script.js 脚本文件,实现隐私 api 的 hook
- 将 frida-gadget.so 与 script.js 写入到本地
- 创建 frida-gadget.config.so 文件,内容结构的 path 指向 script.js 在本地的路径
- 动态加载 frida-gadget.so 文件,该 so 会读取 frida-gadget.config.so 中的 path 路径,获取到 script.js 文件,并执行该 js 脚本
运行效果如下:
该方案的优点不需要 root,并且机型适配比较好,frida 还支持 java/native 的 hook,缺点是,该方案只能针对自己应用进行 Hook。
具体集成方案查看 github FridaCheck 的 README 说明。
总结:
对于上述的几个方案,我还是比较喜欢基于静态方案的 apk smali 扫描与基于动态方案的 frida 无侵入式 camille 方案,这两个方法都无需侵入项目即可实现隐私扫描,适合非开发人员使用。
参考链接: