1
前言目前对于App索权问题越来越重视,先后多个大厂App被下架要求整改:
其中最关键的问题是用户同意隐私协议之前,不能有收集用户隐私信息的行为,例如获取deviceId、androidId等信息,除此之外,对于频繁申请权限、超范围申请权限也是需要注意的。
除了开迭代针对性整改,从技术角度思考,有没有一劳永逸的办法,杜绝隐私调用不合规问题呢?
2
运行时hook技术在隐私整改前期,通过上传apk到史宾格平台,然后平台会安装apk并运行,就能动态监测隐私方法调用,如下图:
完成整个流程,打包-上传-检测,少说也要50分钟~
关于隐私行为实时监控,实现原理无非是利用运行时hook技术,记录方法调用信息。
理论上我们也可以使用运行时hook技术,实现线下快速检测隐私方法调用以及获取调用堆栈的功能。
2.1 Xposed
如果你对Xposed比较熟悉,并且手头有个root的设备安装了Xposed框架,那么直接开发一个Xposed模块来hook指定方法就可以了。
由于我的测试设备是有root权限的,Xposed方案对我来说难度不大,不过对于普通用户,有没有免root的方式呢?
有的~
2.2 VirtualXposed
VirtualXposed 是基于VirtualApp 和 epic 在非ROOT环境下运行Xposed模块的实现(支持5.0~10.0)。
https://github.com/android-hacker/VirtualXposed
VirtualXposed其实就是一个支持Xposed的虚拟机,我们把开发好的Xposed模块和对应需要hook的App安装上去就能实现hook功能。
由于VirtualApp 2017年就闭源转商业,开源版存在不少问题,而且由于其hook大量系统的函数,所以存在不少兼容性问题,有些App安装之后可能打不开,所以如果手头的设备刚好遇到兼容性问题,那可以考虑换个手机啦~
2.3 epic
阿里2014年开源了Dexposed 项目,它能够在Dalvik虚拟机上无侵入地实现运行时方法拦截,但是Android 5.0开始使用ART虚拟机后,不支持ART的Dexposed 就沦为历史。
https://github.com/alibaba/dexposed
之后维术大佬在ART上重新实现了Dexposed,有着与Dexposed完全相同的能力和API,项目地址是epic。
所以如果不想折腾 Xposed 或者 VirtualXposed,只要在应用内接入epic,就可以实现应用内Xposed hook功能,满足运行hook需求。
2.3.1 epic 原理:
原理是通过修改ArtMethod的入口函数,把入口函数的前8个字节修改为一段跳转指令,跳转到执行hook操作的函数,原理跟阿里的热修复框架AndFix差不多,如下图所示。
2.3.2 基于epic 实现一个可配置的运行时hook框架
1. 读取配置:
val inputStream = context.resources.assets.open("privacy_methods.json")
val reader = BufferedReader(InputStreamReader(inputStream))
val result = StringBuilder()
var line: String? = ""
while (reader.readLine().also { line = it } != null) {
result.append(line)
}
val configEntity = Gson().fromJson(result.toString(), PrivacyMethod::class.java)
configEntity.methods.forEach {
hookPrivacyMethod(it)
}
{
"methods": [
{
"name_regex": "android.app.ActivityManager.getRunningAppProcesses",
"message": "读取当前运行应用进程"
},
{
"name_regex": "android.telephony.TelephonyManager.listen",
"message": "监听呼入电话信息"
},
...
]
}
private fun hookPrivacyMethod(entity: PrivacyMethodData) {
if (entity.name_regex.isNotEmpty()) {
val methodName = entity.name_regex.substring(entity.name_regex.lastIndexOf(".") + 1)
val className = entity.name_regex.substring(0, entity.name_regex.lastIndexOf("."))
try {
val lintClass = Class.forName(className)
DexposedBridge.hookAllMethods(lintClass, methodName, object : XC_MethodHook() {
override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam?) {
super.beforeHookedMethod(param)
Log.i(TAG, "beforeHookedMethod $className.$methodName")
Log.d(TAG, "stack= " + Log.getStackTraceString(Throwable()))
}
})
} catch (e: Exception) {
Log.w(TAG, "hookPrivacyMethod:$className.$methodName,e=${e.message}")
}
}
}
4. 运行效果如下:搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
如图所示,运行时输出隐私方法调用堆栈的功能基本实现了,支持通过json配置需要hook的方法。
3
编译时hook技术使用epic只解决了验证隐私方法调用问题,针对如下问题无能为力:
1. release环境如何监控隐私方法调用?
2. 如何管控第三方SDK频繁调用隐私方法问题?
对于这两个问题,可以使用编译时hook技术来解决。
说到编译时hook,首先需要了解编译流程。
3.1 编译流程
我们使用Android Studio开发,使用Gradle 编译工具,对于apk编译流程大家应该都知道,如下图:
apk编译流程无非就是以下这些大的步骤:
1.打包资源文件,生成R.java文件。
2.将AIDL文件编译成java文件。
3.将java文件通过javac命令编译成.class文件。
4.将class文件打包成dex文件。
5.通过apkbuilder工具将dex文件和资源文件打包成apk。
6.apk签名。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
其中第四步(将class文件打包成dex文件),中间就涉及到Gradle的一个Transform流程。
3.2 了解Transform
Transform原理图如下所示:
将class文件、jar文件、资源文件作为输入,经过一系列的Transform处理,首先是自定义的Transform处理,然后是系统的Transform处理,最后一个Transform是负责生成dex文件。
相关源码可以看TaskManager的 createPostCompilationTasks方法,编译流程源码都在这里面~
截图只是贴了自定义Transform的源码,后面还有系统的Transform,例如appliesCustomClassTransforms,用于Profile插件底层实现。
Transform是跟taskFactory关联的,可以这样理解,一个Transform对应Gradle的一个Task。
知道了Transform的大概原理,我们可以通过自定义Plugin,注册一个自定义的Transform到编译流程中去,目的是拿到所有.class文件,再结合ASM 工具修改字节码。
自定义Gradle Plugin,注册Transform,代码如下所示:
class Plugin : Plugin<Project> {
override fun apply(project: Project) {
if (project.plugins.hasPlugin("com.android.application")) {
val extension = project.extensions.getByName("android") as AppExtension
extension.registerTransform(CommonTransform(project))
}
}
}
想要理解为什么自定义插件要这么写,可以看App编译插件源码AppPlugin。
创建AppExtension,name是android,最终是保存到ExtensionsStorage类里面的一个叫extensions的LinkedHashMap变量里面,大家感兴趣可以去看源码。
前面的eproject.extensions.getByName,最终就是从LinkedHashMap中读取的。
拿到.class文件之后,怎么修改呢?这就涉及到修改字节码方案选型。
3.3 字节码修改框架选择
目前主流的字节码修改框架除了ASM,还有Javaassist,两者对比:
由于项目对性能、包体积方面要求比较高,所以无疑采用ASM方案比较合适。
3.4 了解ASM框架
我们通过自定义Transform 能拿到.class文件,之后的字节码处理就通过ASM工具。
Gradle Plugin + Transform ,这套框架的搭建基本都是模板代码,为了节约时间成本和试错成本,本文直接参考dokit,采用boosterapi作为插件的底层实现,booster屏蔽了不同Gradle版本api的差异。
https://github.com/didi/DoraemonKit
https://github.com/didi/booster
说了那么多,最重要的还是要看方案设计~
4
初级hook方案上一步我们通过自定义Transform可以拿到所有.class文件,后面只要通过ClassVistor和MethodVistor,可以分别拿到每个类和方法的字节码,以ActivityManager#getRunningAppProcesses为例,我们要替换成PrivacyUtil#getRunningAppProcesses,流程图如下:
classNode.methods.forEach { method ->
method.instructions?.iterator()?.forEach { insnNode ->
if (insnNode is MethodInsnNode) {
//命中方法,替换
if (insnNode.desc == "android/app/ActivityManager.getRunningAppProcesses ()Ljava/util/List;" &&
insnNode.name == "getRunningAppProcesses" &&
insnNode.opcode == Opcodes.INVOKESPECIAL
) {
//方法指令替换
insnNode.opcode = Opcodes.INVOKESTATIC
//调用类替换
insnNode.owner = "com/lanshifu/asm_plugin_library/privacy/PrivacyUtil"
//方法名替换
insnNode.name = "getRunningAppProcesses"
//参数替换
insnNode.desc = "com/lanshifu/asm_plugin_library/privacy/PrivacyUtil.getRunningAppProcesses (Landroid/app/ActivityManager;)Ljava/util/List;"
}
}
}
}
解释:
通过遍历每个方法的字节码指令,判断是ActivityManager.getRunningAppProcesses这个方法调用,就替换成PrivacyUtil#getRunningAppProcesses调用,涉及到的字节码操作是比较基础的。
tip:为什么要遍历每个方法的字节码指令?因为需要hook的方法是系统的方法,没有被打包到apk中, 单纯遍历方法名是找不到的,必须遍历每个方法里面调用的字节码指令。
到此我们初级版本的编译时隐私方法hook功能就实现了,但是存在几个问题:
相关阅读:2T架构师学习资料干货分享
1、硬编码,不好维护,增加hook方法比较麻烦;
2、对工具类 PrivacyUtil 有依赖,如果后面其它工程使用了这个插件,但是没有引入PrivacyUtil,或者后面插件升级,PrivacyUtil没升级,就会报Class Not Found Exception;
3、开发需要熟悉 ASM 字节码,每次新增一个隐私方法 hook 都需要对比前后字节码变化进行修改验证,麻烦得很;
5
进阶方案想要解决初级方案存在的三个问题,关键在于实现”可配置“,需要在编译期能够读取hook配置,用注解会比较合适。
进阶方案思路如下:
• 用第一个Transform来收集注解信息,生成一份hook配置;
• 用第二个Transform来读取hook配置,替换隐私方法。
5.1 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface AsmMethodReplace {
Class oriClass();
String oriMethod() default "";
int oriAccess() default AsmMethodOpcodes.INVOKESTATIC;
}
注解是对方法生效,需要知道需要hook的方法的类名、方法名、方法类型(静态方法/成员方法)。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。
5.2 注解处理,生成配置
替换一个方法,我们需要的配置如下:
原方法信息(替换前):oriClass、oriMethod、oriAccess、oriDesc
目标方法信息(替换后):targetClass、targetMethod、targetAcces、targetDesc
目标方法信息我们通过ClassNode就能拿到,但是原方法信息,都放到AsmMethodReplace注解上就不太合适了,因为oriDesc写起来比较麻烦, 所以这里约定好一个注解使用规则,然后oriDesc在代码里读取就行了。
规则如下:
1. 对于hook静态方法,注解的方法的参数保持跟原方法一致。
2. 对于hook成员方法,注解的方法的第一个参数是Class对象,之后的参数跟原方法保持一致。
然后oriDesc就通过targetDesc减去第一个参数计算得出。
例如:
targetDesc=(Landroid/telephony/TelephonyManager;)Ljava/lang/String;
通过字符串截取后得到:
oriDesc= Ljava/lang/String;
举个🌰
5.2.1 例子1:hook成员方法
假如要替换掉ActivityManager的getRunningAppProcesses方法。
public List<RunningAppProcessInfo> getRunningAppProcesses() {
try {
return getService().getRunningAppProcesses();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@JvmStatic
@AsmMethodReplace(oriClass = ActivityManager::class, oriAccess = AsmMethodOpcodes.INVOKEVIRTUAL)
fun getRunningAppProcesses(manager: ActivityManager): List<RunningAppProcessInfo?> {
//hook 处理
}
5.2.2 例子2:hook静态方法
假如要替换掉Settings.System的getString方法。
public static String getString(ContentResolver resolver, String name) {
return getStringForUser(resolver, name, resolver.getUserId());
}
@JvmStatic
@AsmMethodReplace(oriClass = Settings.System::class, oriAccess = AsmMethodOpcodes.INVOKESTATIC)
fun getString(resolver: ContentResolver, name: String): String? {
//处理AndroidId
if (Settings.Secure.ANDROID_ID == name) {
}
return Settings.System.getString(resolver, name)
}
详细可以参考文末的源码。
5.3 流程图
最终的流程如上,应该比较清晰了吧~
5.4 注意事项
ASM hook 需要有迹可循,必须明确字节码修改的地方,可以打印log,可以保存记录到文件中,如果出现问题可以从hook日志中排查。
5.5 小结
1. 用一个注解处理的Transform,编译期收集自定义注解信息,生成一份hook配;
2. 用另一个Transform,读取hook配置,hook对应方法;
3. 隐私方法hook之后,增加缓存,解决SDK频繁读取隐私信息问题;
4. 在用户没有同意隐私协议之前,如果调用隐私方法,可以给toast提示,并打印调用堆栈,如下所示,问题一目了然。
6
其它目前大厂也有一些开源的编译时插桩的库,例如饿了么开源的lancet,原理也是 Gradle Plugin+Transform+ASM。
https://github.com/eleme/lancet
如果想深入学习字节码插桩,推荐滴滴开源的dokit,里面有好多字节码操作可以学习,例如大图监控,网络监控等等。
https://github.com/didi/DoraemonKit
由于Gradle 版本更新比较快,大家最好是在项目中尝试自己搭建编译时hook基础框架,这样出问题的话,自己比较好解决,同时也能提升自己字节码开发的技术。
7
总结本文从隐私合规要求作为切入点,大概介绍了如下知识点:
6. 最终实现可配置的编译时方法替换方案,彻底解决隐私方法调用不合规问题。
本文难度其实不算非常大,主要是把Gradle插件和字节码修改的整个流程串起来,涉及到的技术基本都有所提及,最终搭建了一个编译时方法hook框架,之后可以基于这个hook框架做很多东西,例如慢方法检测、全埋点、监控线程调用等~
本文源码:
https://github.com/lanshifu/PrivacyMethodHooker
相关参考文章:
一步步治理隐私权限 | 安卓黑魔法
https://juejin.cn/post/6995015604839137316
一起玩转Android项目中的字节码
https://cloud.tencent.com/developer/article/1378925
去哪儿 Android 客户端隐私安全处理方案
https://mp.weixin.qq.com/s/QJdgI4qeGo8qkTKe0rfVcA#at
booster
https://github.com/didi/booster
作者:蓝师傅
https://juejin.cn/post/7043399520486424612