本文作者:烧麦
前言
笔者之前在云音乐大前端公众号分享了 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 隐衷合规调用就分享的差不多了,然而隐衷合规相干的工作能做的还有很多。
动态的查看也只是辅助咱们定位和查看可能存在的问题。咱们依然能够摸索很多运行时的监测计划,两者互补之后的成果也会更好。
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!