吹爆了:彻底解决隐私方法调用,防止App被下架
Posted River_ly
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了吹爆了:彻底解决隐私方法调用,防止App被下架相关的知识,希望对你有一定的参考价值。
作者:蓝师傅
一、前言
工信部对于App索权问题越来越重视,先后多个大厂App被下架要求整改:
其中最关键的问题是用户同意隐私协议之前,不能有收集用户隐私信息的行为,例如获取deviceId、androidId等信息,除此之外,对于频繁申请权限、超范围申请权限也是需要注意的。
除了开迭代针对性整改,从技术角度思考,有没有一劳永逸的办法,杜绝隐私调用不合规问题呢?
这就是这篇文章要介绍的方案, 前期通过运行时hook技术高效检测隐私方法调用,
后期通过Gradle Plugin
+Transform
+ASM
来hook并替换隐私方法调用,管控App和第三方SDK的隐私行为,彻底解决隐私不合规问题。
二、运行时hook技术
在隐私整改前期,通过上传apk到史宾格平台,然后平台会安装apk并运行,就能动态监测隐私方法调用,如下图:
完成整个流程,打包-上传-检测,少说也要50分钟~
关于隐私行为实时监控,实现原理无非是利用运行时hook技术,记录方法调用信息。
理论上我们也可以使用运行时hook技术,实现线下快速检测隐私方法调用以及获取调用堆栈的功能。
那么运行时hook技术有哪些呢?
2.1 Xposed
如果你对Xposed比较熟悉,并且手头有个root的设备安装了Xposed框架,那么直接开发一个Xposed模块来hook指定方法就可以了。
关于Xposed的源码分析感兴趣可以参考这一篇文章:抱歉,Xposed真的可以为所欲为——终 · 庖丁解码,作者有一系列Xposed文章。
由于我的测试设备是有root权限的,Xposed方案对我来说难度不大,不过对于普通用户,有没有免root的方式呢?
有的~
2.2 VirtualXposed
VirtualXposed 是基于VirtualApp 和 epic 在非ROOT环境下运行Xposed模块的实现(支持5.0~10.0)。
VirtualXposed其实就是一个支持Xposed的虚拟机,我们把开发好的Xposed模块和对应需要hook的App安装上去就能实现hook功能。
由于VirtualApp 2017年就闭源转商业,开源版存在不少问题,而且由于其hook大量系统的函数,所以存在不少兼容性问题,有些App安装之后可能打不开,所以如果手头的设备刚好遇到兼容性问题,那可以考虑换个手机啦~
2.3 epic
阿里2014年开源了Dexposed 项目,它能够在Dalvik虚拟机上无侵入地实现运行时方法拦截,
但是Android 5.0开始使用ART虚拟机后,不支持ART的Dexposed 就沦为历史。
之后维术大佬在ART上重新实现了Dexposed,有着与Dexposed完全相同的能力和API,项目地址是epic 。
所以如果不想折腾 Xposed 或者 VirtualXposed,只要在应用内接入epic,就可以实现应用内Xposed hook功能,满足运行hook需求。
2.3.1 epic 原理:
原理是通过修改ArtMethod的入口函数,把入口函数的前8个字节修改为一段跳转指令,跳转到执行hook操作的函数,原理跟阿里的热修复框架AndFix差不多,如下图所示。
详细原理可以看原文: 我为Dexposed续一秒——论ART上运行时 Method AOP实现
2.3.2 基于epic 实现一个可配置的运行时hook框架
- 读取配置:
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)
- json配置如下,放在assets目录:
"methods": [
"name_regex": "android.app.ActivityManager.getRunningAppProcesses",
"message": "读取当前运行应用进程"
,
"name_regex": "android.telephony.TelephonyManager.listen",
"message": "监听呼入电话信息"
,
...
]
- 根据读取的配置,进行hook
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")
- 运行效果如下:
如图所示,运行时输出隐私方法调用堆栈的功能基本实现了,支持通过json配置需要hook的方法。
tip:epic 存在兼容性问题,例如Android 11 只支持64位App,所以建议只在debug环境使用。
三、编译时hook技术
使用epic只解决了验证隐私方法调用问题,针对如下问题无能为力:
- release环境如何监控隐私方法调用?
- 如何管控第三方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签名
7.apk对齐(可以没有这一步)
其中第四步(将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工具,关于ASM的使用就不介绍了,大家可以参考:
Gradle Plugin
+ Transform
,这套框架的搭建基本都是模板代码,为了节约时间成本和试错成本,本文直接参考dokit,采用booster api作为插件的底层实现,booster屏蔽了不同Gradle版本api的差异。
说了那么多,最重要的还是要看方案设计~
四、初级hook方案
上一步我们通过自定义Transform可以拿到所有.class文件,后面只要通过ClassVistor
和MethodVistor
,可以分别拿到每个类和方法的字节码,
以 ActivityManager#getRunningAppProcesses
为例,我们要替换成 PrivacyUtil#getRunningAppProcesses
,流程图如下:
核心hook代码如下所示:
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功能就实现了,但是存在几个问题:
1、硬编码,不好维护,增加hook方法比较麻烦;
2、对工具类 PrivacyUtil 有依赖,如果后面其它工程使用了这个插件,但是没有引入PrivacyUtil,或者后面插件升级,PrivacyUtil没升级,就会报Class Not Found Exception;
3、开发需要熟悉 ASM 字节码,每次新增一个隐私方法 hook 都需要对比前后字节码变化进行修改验证,麻烦得很;
五、进阶方案
想要解决初级方案存在的三个问题,关键在于实现”可配置“,
需要在编译期能够读取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的方法的类名、方法名、方法类型(静态方法/成员方法)
5.2 注解处理,生成配置
替换一个方法,我们需要的配置如下:
原方法信息(替换前):oriClass、oriMethod、oriAccess、oriDesc
目标方法信息(替换后):targetClass、targetMethod、targetAcces、targetDesc
目标方法信息我们通过ClassNode
就能拿到,但是原方法信息,都放到AsmMethodReplace
注解上就不太合适了,因为oriDesc写起来比较麻烦, 所以这里约定好一个注解使用规则,然后oriDesc在代码里读取就行了。
规则如下:
- 对于hook静态方法,注解的方法的参数保持跟原方法一致
- 对于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 小结
进阶方案主要做了这几件事:
- 用一个注解处理的Transform,编译期收集自定义注解信息,生成一份hook配;
- 用另一个Transform,读取hook配置,hook对应方法;
- 隐私方法hook之后,增加缓存,解决SDK频繁读取隐私信息问题;
- 在用户没有同意隐私协议之前,如果调用隐私方法,可以给toast提示,并打印调用堆栈,如下所示,问题一目了然。
六、其它
目前大厂也有一些开源的编译时插桩的库,例如饿了么开源的lancet,原理也是 Gradle Plugin
+Transform
+ASM
。
如果想深入学习字节码插桩,推荐滴滴开源的dokit,里面有好多字节码操作可以学习,例如大图监控,网络监控等等。
由于Gradle 版本更新比较快,大家最好是在项目中尝试自己搭建编译时hook基础框架,这样出问题的话,自己比较好解决,同时也能提升自己字节码开发的技术。
七、总结
本文从工信部隐私合规要求作为切入点,大概介绍了如下知识点:
- 运行时hook框架介绍和应用
- epic使用和原理
- 编译时hook框架
- 从apk编译流程介绍Transform的原理和应用
- 编译时hook方案对比
- 最终实现可配置的编译时方法替换方案,彻底解决隐私方法调用不合规问题
本文难度其实不算非常大,主要是把Gradle插件和字节码修改的整个流程串起来,涉及到的技术基本都有所提及,最终搭建了一个编译时方法hook框架,之后可以基于这个hook框架做很多东西,例如慢方法检测、全埋点、监控线程调用等~
最后
前段时间还收集整理了Android高工必备技能知识脑图和核心知识点笔记文档!既能够夯实底层原理核心技术点,又能够掌握普通开发者,难以触及的架构设计方法论。那你在工作中、团队里、面试时,也就拥有了同行难以复制的核心竞争力。
相关的一些知识点解析都已经做了收录整理上传至公号中:Android开发之家,大家可以自行访问查阅。
以上是关于吹爆了:彻底解决隐私方法调用,防止App被下架的主要内容,如果未能解决你的问题,请参考以下文章