彻底解决隐私方法调用,防止 App 被下架

上一篇:离开互联网上岸1年后,我后悔了!重回大厂内卷


1

前言

目前对于App索权问题越来越重视,先后多个大厂App被下架要求整改:

彻底解决隐私方法调用,防止 App 被下架_第1张图片

 其中最关键的问题是用户同意隐私协议之前,不能有收集用户隐私信息的行为,例如获取deviceId、androidId等信息,除此之外,对于频繁申请权限、超范围申请权限也是需要注意的。

除了开迭代针对性整改,从技术角度思考,有没有一劳永逸的办法,杜绝隐私调用不合规问题呢?

这就是这篇文章要介绍的方案, 前期通过运行时hook技术高效检测隐私方法调用,后期通过Gradle Plugin+Transform+ASM 来hook并替换隐私方法调用,管控App和第三方SDK的隐私行为,彻底解决隐私不合规问题。

2

运行时hook技术

在隐私整改前期,通过上传apk到史宾格平台,然后平台会安装apk并运行,就能动态监测隐私方法调用,如下图:

彻底解决隐私方法调用,防止 App 被下架_第2张图片

完成整个流程,打包-上传-检测,少说也要50分钟~

关于隐私行为实时监控,实现原理无非是利用运行时hook技术,记录方法调用信息。

理论上我们也可以使用运行时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。

https://github.com/tiann/epic

所以如果不想折腾 Xposed 或者 VirtualXposed,只要在应用内接入epic,就可以实现应用内Xposed hook功能,满足运行hook需求。

2.3.1 epic 原理:

原理是通过修改ArtMethod的入口函数,把入口函数的前8个字节修改为一段跳转指令,跳转到执行hook操作的函数,原理跟阿里的热修复框架AndFix差不多,如下图所示。

彻底解决隐私方法调用,防止 App 被下架_第3张图片

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)
}

2. json配置如下,放在assets目录:

{
  "methods": [
    {
      "name_regex": "android.app.ActivityManager.getRunningAppProcesses",
      "message": "读取当前运行应用进程"
    },
    {
      "name_regex": "android.telephony.TelephonyManager.listen",
      "message": "监听呼入电话信息"
    },
    ...
  ]
}

3. 根据读取的配置,进行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}")
        }
    }
}

4. 运行效果如下:搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。

彻底解决隐私方法调用,防止 App 被下架_第4张图片

如图所示,运行时输出隐私方法调用堆栈的功能基本实现了,支持通过json配置需要hook的方法。

tip:epic 存在兼容性问题,例如Android 11 只支持64位App,所以建议只在debug环境使用。

3

编译时hook技术

使用epic只解决了验证隐私方法调用问题,针对如下问题无能为力:

1. release环境如何监控隐私方法调用?

2. 如何管控第三方SDK频繁调用隐私方法问题?

对于这两个问题,可以使用编译时hook技术来解决。

说到编译时hook,首先需要了解编译流程。

3.1 编译流程

我们使用Android Studio开发,使用Gradle 编译工具,对于apk编译流程大家应该都知道,如下图:

彻底解决隐私方法调用,防止 App 被下架_第5张图片

apk编译流程无非就是以下这些大的步骤:


1.打包资源文件,生成R.java文件。


2.将AIDL文件编译成java文件。


3.将java文件通过javac命令编译成.class文件。


4.将class文件打包成dex文件。


5.通过apkbuilder工具将dex文件和资源文件打包成apk。


6.apk签名。搜索公众号互联网架构师回复“2T”,送你一份惊喜礼包。


7.apk对齐(可以没有这一步)。

其中第四步(将class文件打包成dex文件),中间就涉及到Gradle的一个Transform流程。

3.2 了解Transform

Transform原理图如下所示:

彻底解决隐私方法调用,防止 App 被下架_第6张图片

将class文件、jar文件、资源文件作为输入,经过一系列的Transform处理,首先是自定义的Transform处理,然后是系统的Transform处理,最后一个Transform是负责生成dex文件。

相关源码可以看TaskManager的 createPostCompilationTasks方法,编译流程源码都在这里面~

彻底解决隐私方法调用,防止 App 被下架_第7张图片

截图只是贴了自定义Transform的源码,后面还有系统的Transform,例如appliesCustomClassTransforms,用于Profile插件底层实现。

Transform是跟taskFactory关联的,可以这样理解,一个Transform对应Gradle的一个Task

知道了Transform的大概原理,我们可以通过自定义Plugin,注册一个自定义的Transform到编译流程中去,目的是拿到所有.class文件,再结合ASM 工具修改字节码。

自定义Gradle Plugin,注册Transform,代码如下所示:

class Plugin : Plugin {

    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。

彻底解决隐私方法调用,防止 App 被下架_第8张图片

创建AppExtension,name是android,最终是保存到ExtensionsStorage类里面的一个叫extensions的LinkedHashMap变量里面,大家感兴趣可以去看源码。

前面的eproject.extensions.getByName,最终就是从LinkedHashMap中读取的。

拿到.class文件之后,怎么修改呢?这就涉及到修改字节码方案选型。

3.3 字节码修改框架选择

目前主流的字节码修改框架除了ASM,还有Javaassist,两者对比:

彻底解决隐私方法调用,防止 App 被下架_第9张图片

由于项目对性能、包体积方面要求比较高,所以无疑采用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,流程图如下:

彻底解决隐私方法调用,防止 App 被下架_第10张图片

核心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功能就实现了,但是存在几个问题:

相关阅读: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 getRunningAppProcesses() {
    try {
        return getService().getRunningAppProcesses();
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

由于这个是成员方法,那么注解的写法如下:

@JvmStatic
@AsmMethodReplace(oriClass = ActivityManager::class, oriAccess = AsmMethodOpcodes.INVOKEVIRTUAL)
fun getRunningAppProcesses(manager: ActivityManager): List {
    //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 流程图

彻底解决隐私方法调用,防止 App 被下架_第11张图片

最终的流程如上,应该比较清晰了吧~

5.4 注意事项

ASM hook 需要有迹可循,必须明确字节码修改的地方,可以打印log,可以保存记录到文件中,如果出现问题可以从hook日志中排查。

彻底解决隐私方法调用,防止 App 被下架_第12张图片

5.5 小结

进阶方案主要做了这几件事:

1. 用一个注解处理的Transform,编译期收集自定义注解信息,生成一份hook配;

2. 用另一个Transform,读取hook配置,hook对应方法;

3. 隐私方法hook之后,增加缓存,解决SDK频繁读取隐私信息问题;

4. 在用户没有同意隐私协议之前,如果调用隐私方法,可以给toast提示,并打印调用堆栈,如下所示,问题一目了然。

彻底解决隐私方法调用,防止 App 被下架_第13张图片

6

其它

目前大厂也有一些开源的编译时插桩的库,例如饿了么开源的lancet,原理也是 Gradle Plugin+Transform+ASM。

https://github.com/eleme/lancet

如果想深入学习字节码插桩,推荐滴滴开源的dokit,里面有好多字节码操作可以学习,例如大图监控,网络监控等等。

https://github.com/didi/DoraemonKit

由于Gradle 版本更新比较快,大家最好是在项目中尝试自己搭建编译时hook基础框架,这样出问题的话,自己比较好解决,同时也能提升自己字节码开发的技术。

7

总结

本文从隐私合规要求作为切入点,大概介绍了如下知识点:

1. 运行时hook框架介绍和应用。

2. epic使用和原理。

3. 编译时hook框架。

4. 从apk编译流程介绍Transform的原理和应用。

5. 编译时hook方案对比。

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

1、2T架构师学习资料干货分享

2、985副教授工资曝光

3、心态崩了!税前2万4,到手1万4,年终奖扣税方式1月1日起施行~

4、雷军做程序员时写的博客,很强大!

5、人脸识别的时候,一定要穿上衣服啊!

6、清华大学:2021 元宇宙研究报告!

7、绩效被打3.25B,员工将支付宝告上了法院,判了

你可能感兴趣的:(android,java,编程语言,jvm,安卓)