• ASM字节码插桩解决国内隐私问题


    背景

    2021年政府加强了对用户隐私的保护,App 的上架更新必须通过隐私合规扫描,而对 App 开发者来说就是必须在用户同意隐私政策前不能调用政府规定的隐私相关的 api。对于大家自己项目内部的代码来说,可以自己手动改掉,但是对于一些用到的第三方库就束手无策了,也许这些库有隐私版本的更新,但是对大家来说,可能版本差距太大,项目改动会比较多,当公司老项目比较多时,这种修改方案就耗费时间太久了。

    而使用Gradle插件+ASM的方式可以很好的解决我们的困难。

    ASM 是什么

    ASM是AOP编程中一种比较成熟的框架,而 AOP是面向切面编程,具体概念比较复杂,我暂时理解为函数插桩,简单来说就是在代码中插入代码

    ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但侧重于性能。由于它的设计和实现尽可能小和快,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)

    ASM 中我们主要用到下面几个类

    **ClassReader:**一个解析器,可以让ClassVisitor像访问jvm中的Java文件一样访问class文件的结构

    **ClassVisitor:**Java class文件的访问器,可以访问class文件的结构

    **MethodVisitor:**Java 方法的访问器

    **ClassWriter:**一个类访问器ClassVisitor,可以生成相应的class文件结构。

    这里我们暂时只要知道 ASM 是一个可以修改 java .class文件的库就可以了

    Gradle插件 Transform API

    Android 的打包流程如下图

    请添加图片描述

    1、使用aapt工具将res资源文件生成R.java文件
    2、使用aidl工具将aidl文件生成对应java文件
    3、使用javac命令编译工程源代码和上面两步生成的文件,生成class文件
    4、通过dex工具将class文件和第三方jar包打成dex文件
    5、用res下编译过的二进制文件,以及assets中的原始资源文件,以及dex文件,通过apkbuilder工具打包成apk文件
    6、通过jarsigner对apk进行签名
    7、利用zipalign工具对apk进行字节对齐优化操作

    在上面第3-4步的过程中,如果我们对.class 文件使用 ASM 进行处理就可以做到对任意 .class文件的修改,如下图

    img

    而 Android Studio 使用的构建工具 Gradle 是可以让我们自定义插件的,自定义 Gradle 插件有一个Transform API 可以实现让我们对所有的.class进行处理

    关于如何自定义插件可以看这篇文章https://www.jianshu.com/p/d14f24c4a807

    关于Transform API 可以参考这个文章http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/

    自定义 Gradle 插件和Transform API 不是我们这篇文章的内容,看到这里默认大家已经对这些知识有一定了解了,不知道的请看上面文章

    下面是插件实现的代码

    public class HookPlugin implements Plugin<Project> {
    
      @SuppressWarnings("NullableProblems")
      @Override
      public void apply(Project project) {
        AppExtension appExtension = (AppExtension) project.getProperties().get("android");
        //注册一个Transform
        appExtension.registerTransform(new HookTransform(project), Collections.EMPTY_LIST);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Hunter库

    Hunter是这么一个框架,帮你快速开发插件,在编译过程中修改字节码,它底层基于ASMGradle Transform API 实现。…

    仓库地址:https://github.com/Leaking/Hunter

    其实这个库就是对 ASM 和 Gradle 的再一层封装,让开发者更加将更多重心放在需要实现的业务上

    我们在自定义Transform时,通过继承Hunter库里的 HunterTransform来实现,它对 ASM 的功能进行了封装

    然后我们在自定义的HunterTransform里提供一个BaseWeaver类即可做到对字节码的访问

    public final class HookTransform extends HunterTransform {
      private HookExtension hookExtension;
    
      public HookTransform(Project project) {
        super(project);
        //这个类用来存储插件的提供给用户的配置信息
        hookExtension = project.getExtensions().create("macHookExt", HookExtension.class);
        //我们需要给父类的bytecodeWeaver属性一个赋值,BaseWeaver帮我们封装了对压缩文件等的访问,
        // 我们只需要提供一个ClassVisitor来决定如何对一个 class 文件访问就可以
        this.bytecodeWeaver = new HookWeaver(hookExtension);
        HookLog.info("HookExtension:" + hookExtension.toString());
      }
    
      @Override
      public void transform(Context context, Collection<TransformInput> inputs,
          Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider,
          boolean isIncremental) throws IOException, TransformException, InterruptedException {
    
          bytecodeWeaver.setExtension(hookExtension);
        HookLog.info("HookExtension:" + hookExtension.toString());
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    BaseWeaver帮我们封装了对压缩文件(jar文件)等的访问,我们只需要提供一个ClassVisitor来决定如何对一个 class 文件访问就可以

    public final class HookWeaver extends BaseWeaver {
    
      private HookExtension hookExtension;
    
      public HookWeaver(HookExtension hookExtension) {
        this.hookExtension = hookExtension;
      }
    
      /**
       * @param fullQualifiedClassName class的全限定名
       * 是否要访问这个 class 文件
       */
      @Override
      public boolean isWeavableClass(String fullQualifiedClassName) {
        return super.isWeavableClass(fullQualifiedClassName);
      }
    
      /**
       * @return 提供一个类的访问者
       */
      @Override
      protected ClassVisitor wrapClassWriter(ClassWriter classWriter) {
        //这是一个我们自定义的ClassVisitor,对 class 的访问业务就写在这里
        return new HookClassAdapter(classWriter, hookExtension);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    业务实现

    上面我们提到我们对class 的访问业务写在HookClassAdapter这个 ClassVisitor 里,现在我们就看下它是如何实现的。

    public final class HookClassAdapter extends ClassVisitor {
    
      private String className = "";
      private String superClassName = "";
      private HookExtension hookExtension;
    
      HookClassAdapter(final ClassVisitor cv, HookExtension hookExtension) {
        super(Opcodes.ASM6, cv);
        this.hookExtension = hookExtension;
      }
    
      /**
         * 用于标记是否拦截对这个类的访问
         */
      private boolean needIntercept = false;
    
      /**
         * 访问类的标题
         */
      @Override
      public void visit(int version, int access, String name, String signature, String superName,
                        String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    
        if (name != null) {
          this.className = name.replace("/", ".");
        }
        if (superName != null) {
          this.superClassName = superName.replace("/", ".");
        }
    
    
        // 过滤自定义接口
        needIntercept = false;
        for (String face : interfaces) {
          if (HOOK_INTERFACE_PATH.equals(face.replace("/", ".")) || HOOK_INTERFACE_PATH2.equals(face.replace("/", "."))) {
            needIntercept = true;
            break;
          }
        }
        //过滤插件中使用到的相关类
        //|| HOOK_INTERFACE_ADAPTER_PATH.equals(face.replace("/", ".")
        if (HOOK_INTERFACE_ADAPTER_PATH.equals(superClassName)
            || HOOK_INTERFACE_ADAPTER_PATH.equals(className)
            || HOOK_INTERFACE_ADAPTER_PATH2.equals(superClassName)
            || HOOK_INTERFACE_ADAPTER_PATH2.equals(className)
           ) {
          needIntercept = true;
        }
    
        if (HOOK_CLASS.equals(className)) {
          needIntercept = true;
        }
      }
    
      /**
         * 提供一个用于访问访问方法的 MethodVisitor
         */
      @Override
      public MethodVisitor visitMethod(final int access, final String name,
                                       final String desc, final String signature, final String[] exceptions) {
        if (needIntercept) {
          //需要跳过的类就不走我们自己的MethodVisitor了
          return super.visitMethod(access, name, desc, signature, exceptions);
        }
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        //提供一个我们自己处理业务的MethodVisitor
        return mv == null ? null
          : new HookMethodAdapter(className, superClassName, name, access, desc, mv, hookExtension);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    HookClassAdapter继承了一个ClassVisitor,这里我们主要覆写了 visit 方法和 visitMethod 方法。如上面注释, visit 方法用于访问类的标题,我们可以在这里获取当前访问 class 的类名等信息,

    注意,ClassVisitor使用了访问者模式,可以简单的理解为,通过ClassVisitor我们就可以访问 class 文件的各个部分

    一些解释看这些文章

    https://www.jianshu.com/p/e4b8cb0b3204

    https://blog.51cto.com/u_15064638/2874093

    https://wenku.baidu.com/view/391d899e1937f111f18583d049649b6648d709c9.html

    visitMethod 主要是提供一个用于访问方法的 MethodVisitor,和这里的 ClassVisitor 类似,不过MethodVisitor用于处理对方法的访问

    public final class HookMethodAdapter extends LocalVariablesSorter implements Opcodes {
      public final class HookMethodAdapter extends LocalVariablesSorter implements Opcodes {
        private String classNamePath;
        private String className;
        private String superClassName;
        private String methodDes;
        private String methodName;
        private HookExtension hookExtension;
    
        public HookMethodAdapter(String className, String superClassName, String methodName, int access,
                                 String desc,
                                 MethodVisitor mv, HookExtension hookExtension) {
          super(Opcodes.ASM5, access, desc, mv);
          this.classNamePath = className.replace(".", "/");
          this.className = className;
          this.superClassName = superClassName;
          this.methodName = methodName;
          this.methodDes = desc;
          this.hookExtension = hookExtension;
        }
    
        @Override
        public void visitMethodInsn(int opcode, String owner, String methodName, String desc, boolean itf) {
          //判断是在调用WifiInfo的getMacAddress方法
          if ("android/net/wifi/WifiInfo".equals(owner) && ("getMacAddress").equals(methodName) && "()Ljava/lang/String;".equals(desc)) {
            //修改为调用HOOK_CLASS_PATH(com.quvideo.mobile.platform.machook.MacHook)类的getTestMac方法
            mv.visitMethodInsn(INVOKESTATIC, HookExtension.HOOK_CLASS_PATH, "getTestMac", "(L" + owner + ";)Ljava/lang/String;", false);
            return;
          }
          ...
          super.visitMethodInsn(opcode, owner, methodName, desc, itf);
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    上面的代码完成业务是 使用我们自定义的MacHook.getTestMac方法 替换WifiInfo调用getMacAddress方法获取 mac 地址,将每个地方这样的调用都替换成调用我们自己的方法,然后再在我们自定义方法里编写逻辑,比如隐私同意前给一个空值,隐私同意后再调用WifiInfogetMacAddress获取真实的地址。

    上面的owner表示类名,methodName就是方法名,desc是方法的描述,表示结构为([参数类型],[参数类型]...)返回类型;

    参数类型和返回类型要用字节码的表示方式,可以参考下面两张图

    请添加图片描述

    请添加图片描述

    帮助插件

    visitMethodInsn 的各种代码看不懂,写不出来怎么办,不用怕用ASM Bytecode Viewer Support Kotlin插件自动生成

    请添加图片描述

    我们再简单看下插件怎么用

    找到你要修改的类,在编辑界面右键,然后在菜单中选择ASM Bytecode Viewer

    请添加图片描述

    然后 AndroidStudio 会开始 build,等 build 结束后会弹出如下窗口

    请添加图片描述

    这里显示的字节码指令,红色划线处的意思是 调用类为android/net/wifi/WifiInfogetMacAddress方法,方法描述为()Ljava/lang/String;这里就与上面的判断逻辑对上了

    而我们要改成的代码则这样操作,先写好想要调用的代码,比如下面

    void test() {
      WifiInfo wifiInfo = null;
      MacHook.getTestMac(wifiInfo);
    }
    
    • 1
    • 2
    • 3
    • 4

    然后再次用 ASM Bytecode Viewer插件

    请添加图片描述

    可以发现红线处就是我们要实现业务写的代码,是不是 so easy

    补充

    其他的比如插件的发布等大家可以自己找下插件相关的文章看下,这里就不多做介绍了

    本次描述的插件下 build.gradle 配置如下

    apply plugin: 'groovy'
    
    dependencies {
      implementation gradleApi()
      implementation localGroovy()
      implementation 'com.android.tools.build:gradle:3.1.4'
      implementation 'com.android.tools.build:gradle-api:3.1.4'
      implementation 'commons-io:commons-io:2.6'
      implementation 'commons-codec:commons-codec:1.10'
      //noinspection GradleDependency
      implementation 'org.ow2.asm:asm:6.2.1'
      //noinspection GradleDependency
      implementation 'org.ow2.asm:asm-util:6.2.1'
      //noinspection GradleDependency
      implementation 'org.ow2.asm:asm-commons:6.2.1'
      implementation 'com.quinn.hunter:hunter-transform:0.9.0'
    }
    
    repositories {
      google()
      jcenter()
      mavenCentral()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    参考以下文章

    具体原理啥的可以看下这里https://zhuanlan.zhihu.com/p/359299148

    http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/

    https://www.jianshu.com/p/e3ca9ca3a3d3

    https://maimai.cn/article/detail?fid=1674572771&efid=xtzpWBxgH0Iv9zVTBsZ_Ww

    er:hunter-transform:0.9.0’
    }

    repositories {
    google()
    jcenter()
    mavenCentral()
    }

    
    ### 参考以下文章
    
    具体原理啥的可以看下这里https://zhuanlan.zhihu.com/p/359299148
    
    http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/
    
    https://www.jianshu.com/p/e3ca9ca3a3d3
    
    https://maimai.cn/article/detail?fid=1674572771&efid=xtzpWBxgH0Iv9zVTBsZ_Ww
    
    ASM 讲解视频 https://www.bilibili.com/video/BV1Py4y1g7dN/?spm_id_from=333.788&vd_source=03ff970824e8707a9d0450978c8351ec
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    C#创建并启动新的进程
    TS:知识补充
    redis的缓存更新策略
    苏州科技大学、和数联合获得国家知识产权局颁发的3项发明专利证书
    el-table通过这样封装可以实现校验-表格校验的原理
    Linux Podman安装DVWA靶场环境
    百度SEO优化全攻略(提高网站排名的5个方面)
    分布式锁java程序怎么处理 zk与redis
    职场人,该看重机遇,还是该注重自己的能力?
    Java编写xml文件时,文件中特殊字符如何解决?
  • 原文地址:https://blog.csdn.net/qq_19269585/article/details/125465901