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);
  }
}
Hunter库

Hunter是这么一个框架,帮你快速开发插件,在编译过程中修改字节码,它底层基于ASM 和 Gradle 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);
  }
}

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);
  }
}
业务实现

上面我们提到我们对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);
  }
}

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

上面的代码完成业务是 使用我们自定义的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);
}

然后再次用 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()
}
参考以下文章

具体原理啥的可以看下这里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


参考以下文章

具体原理啥的可以看下这里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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值