dexguard

Project Url: Ivonhoe/dexguard
Introduction: Android app 防 dex2jar 的 gradle 插件
More: Author   ReportBugs   
Tags:

TODO LIST

  • plugin 支持增量编译
  • 以子之矛攻子之盾

如何使用

  • 在 root project 的 build.gradle 中添加依赖classpath 'ivonhoe.gradle.dexguard:dexguard-gradle:0.0.4-SNAPSHOT'
buildscript {
    repositories {
        maven { url 'https://raw.githubusercontent.com/Ivonhoe/mvn-repo/master/' }
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0'
        classpath 'ivonhoe.gradle.dexguard:dexguard-gradle:0.0.4-SNAPSHOT'
    }
}
  • 在 app 项目的 build.gradle 中添加插件,map.txt 中配置需要保护的方法名
apply plugin: 'ivonhoe.dexguard'
dexguard {
    guardConfig = "${rootDir}/map.txt"
}

一、概述

上一篇文章,我大致分析了美团外卖 Android 客户端是如何防止其 Java 代码被 dex2jar 工具转换,而做的防护,那就是将有语法错误的字节码插入到想要保护的 Java 函数中中,借助 dex2jar 的语法检查机制,以达到 dex2jar 转换出错的目的。这篇文章就大致记录下如何开发 Gradle 编译插件,在编译过程中实现上述防护思路。

二、思路

先看下 Android APK 打包流程:

Android apk 打包流程

Android APK 打包流程如上图所示,Java 代码先通过 Java Compiler 生成.class 文件,在通过 dx 工具生成 dex 文件,最后使用 apkbuilder 工具完成代码与资源文件的打包,并使用 jarsigner 签名,最后可能还有使用 zipalign 对签名后的 apk 做对齐处理。

如果需要完成对特定函数的代码注入,可以在 Java 代码编译生成 class 文件后,在 dex 文件生成前,针对 class 字节码进行操作,以本例为例需要动态生成 Exsit 类文件的字节码(不清楚 Exsit 的作用可以看上一篇文章)。

// 动态生成 Exist.class
public class Exist {
    public static boolean a() {
        return false;
    }

    public static void b(int test) {
    }
}

动态修改特定方法的字节码,将下列 Java 代码转换成字节码插入特定的函数中。

// 插入到特定的 Java 函数内
Exist.b(Exist.a());

并将修改后的.class 文件放入 dex 打包目录中,完成 dex 打包,具体流程如下图所示:

Gradle 提供了叫Transform的 API,允许三方插件在 class 文件转换为 dex 文件前操作编译好的 class 文件,这个 API 的目标就是简化 class 文件的自定义的操作而不用对 Task 进行处理,并且可以更加灵活地进行操作。详细的可以参考区长的博客

四、ASM 操作 Java 字节码

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直 接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。这里推荐一个 IDEA 插件:ASM ByteCode Outline。可以查看.class 文件的字节码,并可以生成成 ASM 框架代码。安装ASM Bytecode Outline插件后,可以在Intellij IDEA->Code->Show Bytecode Outline查看类文件对应个字节码和 ASM 框架代码,利用 ASM 框架代码就可以生成相应的.class 文件了。

生成 Exist 字节码的具体实现,生成 Exist.java 的构造函数:

ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

cw.visit(51, ACC_PUBLIC + ACC_SUPER, "ivonhoe/dexguard/java/Exist", null, "java/lang/Object", null);

cw.visitSource("Exist.java", null);

mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(7, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Livonhoe/dexguard/java/Exist;", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();

声明一个函数名为 a,返回值为 boolean 类型的无参函数:

mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "a", "()Z", null, null);
mv.visitCode();
l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(10, l0);
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();

声明一个函数名为 b,参数为 int 型,返回类型为 void 的函数

MV = CW.VISITmETHOD(acc_public + acc_static, "b", "(i)v", NULL, NULL);
MV.VISITcODE();
L0 = NEW lABEL();
MV.VISITlABEL(L0);
MV.VISITlINEnUMBER(14, L0);
MV.VISITiNSN(return);
L1 = NEW lABEL();
MV.VISITlABEL(L1);
MV.VISITlOCALvARIABLE("TEST", "i", NULL, L0, L1, 0);
MV.VISITmAXS(0, 1);
MV.VISITeND();

在指定函数内,插入Exist.b(Exist.a());对应的字节码的具体实现,绕过 Java 编译器的语法检查:

//refer hack class when object init
private static byte[] referHackWhenInit(InputStream inputStream, String methodName) {
    ClassReader cr = new ClassReader(inputStream);
    ClassWriter cw = new ClassWriter(cr, 0);

    ClassVisitor cv = new InjectCassVisitor(Opcodes.ASM4, cw, methodName);
    cr.accept(cv, 0);
    return cw.toByteArray();
}
static class InjectClassVisitor extends ClassVisitor {

        private String methodName;

        InjectClassVisitor(int i, ClassVisitor classVisitor, String method) {
            super(i, classVisitor)

            this.methodName = method;
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {

            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
            mv = new MethodVisitor(Opcodes.ASM4, mv) {

                @Override
                void visitCode() {
                    // 在方法体开始调用时
                    if (name.equals(methodName)) {
                        mv.visitMethodInsn(INVOKESTATIC, "ivonhoe/dexguard/java/Exist", "a", "()Z", false);
                        mv.visitMethodInsn(INVOKESTATIC, "ivonhoe/dexguard/java/Exist", "b", "(I)V", false);
                    }
                    super.visitCode()
                }

                @Override
                public void visitMaxs(int maxStack, int maxLocal) {
                    if (name.equals(methodName)) {
                        super.visitMaxs(maxStack + 1, maxLocal);
                    } else {
                        super.visitMaxs(maxStack, maxLocal);
                    }
                }
            }
            return mv;
        }
    }

五、源码

详细的 Gradle 源码和实例可参考https://github.com/Ivonhoe/dexguard

六、参考文档

Android 热修复使用 Gradle Plugin1.5 改造 Nuwa 插件

ASM-操作字节码初探

手摸手增加字节码往方法体内插代码

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools