前言
笔者之前在云音乐大前端公众号分享了 Android 隐私合规静态检查的一部分实现。
Android隐私合规静态检查
上一篇文章通过反编译 APP 的方式,扫描了 APP 内对隐私方法调用的检查。但存在一些问题:
- 无法检查到 so 文件里是否可能存在隐私方法的调用。
- 当我们全量扫描出某个地方存在隐私方法调用的时候,我们不知道它实际的调用的入口究竟在哪里。
so 文件里的调用
有时候我们有一些隐私方法是通过 JNI 反射执行 Java 层代码调用的,无法通过扫描 Java 层文件找到。所以需要针对 so 文件做一个特殊处理。
我们来梳理一下我们的需求:对于 APP 业务方,一般来说只需要知道某些隐私方法有没有通过 so 调用。在哪个 so 里可能会存在调用。剩下的,我们交给 so 的开发者去排查就行了。
需求明确了,那我们怎么知道 so 文件里是否调用了某个方法呢?在 Java 中,如果通过反射调用方法,类名+方法名的字符串肯定是作为字符串常量存在 class 文件的常量池内。那么 so 里是否会有类似的存储方式呢?
答案是肯定的,linux C 程序的字符串可能存在于以下 2 个区域:
- .text 代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
- .rodata 该段也叫常量区,用于存放常量数据,ro 就是 ReadOnly 的意思。存放 C 中的字符串和 #define 定义的常量.
我们可以通过 linux 的 strings
命令,来获取 so 文件里面使用到的字符串:
strings xx.so
我们检查 apk 文件里每个 so 文件的字符串,如果能匹配上配置的隐私方法名,那么就把当前的 so 标记为可疑的调用。检查的流程如下图:
检查输出结果参考下面的 demo 示图:
方法调用链分析
很多时候我们不知道是哪里调用了某个 Android API, 一般只能通过运行时去处理一下,例如 hook 这个方法替换它的实现。但是运行时检查覆盖不了所有的场景。所以静态检查 apk 的方法调用链是很必要的。至少我们可以看到某个敏感方法的调用源头是哪个类,从而进行溯源和归因。
笔者在上一篇分享的技术方案基础之上,进一步分析了方法调用链。上篇文章我们说到了通过反编译 apk,我们能转换生成相关的 smali 文件,smali 文件里会存在相关的方法调用信息。我们可以通过这些方法信息将整个 app 的方法调用关系组织起来。
方法收集
在 smali 文件的开头,会标记当前类的相关信息:
.class public final Lokhttp3/OkHttp;
.super Ljava/lang/Object;
我们会获取到当前一个类的修饰符和完整的类型描述符。
smali 里的 .method
指令则描述了当前 class 里有哪些方法:
.method constructor <init>(Lokhttp3/Call$Factory;Lokhttp3/HttpUrl;Ljava/util/List;Ljava/util/List;Ljava/util/concurrent/Executor;Z)V
.method private validateServiceInterface(Ljava/lang/Class;)V
.method public baseUrl()Lokhttp3/HttpUrl;
这里以 Retrofit
为例,我们可以看到 Retrofit.smali
里面的方法描述:
- 构造方法,传入的参数为 Factory、HttpUrl、List、List、Executor 和 boolean
- 私有方法 validateServiceInterface,参数为 Class,返回 void
- 公开方法 baseUrl,无参数,返回 HttpUrl
通过上述这些信息,我们可以收集到一个 APP 内,所有的方法。我们需要为每个方法建立自己的可识别性,我们通过下面这些字段来进行判断:
- 方法定义所在的类,需要是完整的包名+类名
一个方法签名内需要的字段,包括:
- 方法名
- 传入的参数
在 smali 中,方法的描述符是使用的 jvm 的描述符,我们需要解析描述符里的信息,来保存我们的每个字段以备输出显示。
方法的描述符规则会把符号和类型对应起来,基本类型的关系为:
|符号|类型|
|---|---|
|V|void|
|Z|boolean|
|S|short|
|C|char|
|I|int|
|J|long|
|F|float|
|D|double|
对象则表示为完整的包名和类名,L
开头,使用文件描述符间隔,使用分号结尾,例如 Strig:
LJava/lang/String;
方法关系建立
收集到了所有的方法,我们建立调用链就还需要知道,方法调用了谁,以及方法被谁调用了。
在 smali 中,我们可以通过 invoke-
指令找到某个方法内调用了哪些其他方法:
invoke-
包括
invoke-direct
直接调用某个方法invoke-static
调用某个 static 方法invoke-virtual
调用某个虚方法invoke-super
直接调用父类的虚方法invoke-interface
调用某个接口的方法
除了 invoke-interface
需要在运行时确认调用对象,其他几个是可以通过 invoke-
后面的描述部分知道当前方法调用了哪些方法:
invoke-virtual {v2, p2, v1}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
invoke-
后半段指令描述了具体调用的类名和方法,使用 -> 分隔开。解析这部分指令,我们可以获取到被调用方法的完整信息。
我们可以通过对整个 app 内反编译出的 smali 文件的调用关系进行一个收集,收集过程中,每个方法都会被存储下来,每个方法除了自己的方法信息,还包括被调用的列表:
- calleds: 调用了自己的方法列表
当某个方法调用被扫描到的时候,我们会把这个方法添加到当前调用者的 callers 里面,同时也把调用者添加到自己的 calleds 里面去。最终方法关系就建立成如下图所示:
我们最终建立了一颗多叉树的图结构,这张图里,我们可以把我们需要检查调用链的隐私方法看做是树的叶子节点。
当然,我们也可以再新增一个 callers 数组,来表示每个方法调用的方法列表,这样我们还可以建立一个节点点存在双向绑定关系的树结构:
在双向绑定的树结构中,我们既可以根据某个方法去分析出这个方法的调用链。也可以从顶层开始,分析某些入口所有可能存在的调用链。
例如,当我们怀疑某些页面存在不合规的调用时,我们可以把这些 Activity 的类找到,从上往下去寻找是否调用了隐私方法。
调用链遍历
方法调用的关系建立完毕后,我们需要遍历出所有的调用链并输出给使用方。这里就比较简单了,我们可以使用深度优先遍历来寻找我们的所有可能的路径:
这里存在一种特殊情况,在递归的时候,有可能会出现 A 被 B 调用, B 又被 A 调用的情况,反映到当前的数据结构就是图结构形成了环。所以我们需要针对是否存在环进行判断。
当我们判断到当前调用链上存在重复节点的时候,就可以认定为存在环。这时候可以直接结束这条链上的递归,实际上也并不会影响我们事后分析这条调用链的合规性。
这部分逻辑可以用伪代码来表示:
fun traversal(method) {
val paths = []
dfs(method, [], paths)
}
fun dfs(method, path, temp) {
if (method.calleds.isNotEmpty) {
for (called in method.calleds) {
if (path.contains(called)) {
temp.add(path)
continue
} else {
newPath = []
newPath.addAll(path)
newPath.add(0, method)
dfs(called.method, newPath, temp)
}
}
} else {
path.add(0, method)
temp.add(path)
}
}
调用链分析最后的效果如下图:
总结
到这里静态检查 Android 隐私合规调用就分享的差不多了,但是隐私合规相关的工作能做的还有很多。
静态的检查也只是辅助我们定位和检查可能存在的问题。我们仍然可以探索很多运行时的监测方案,两者互补之后的效果也会更好。