ASM插桩举例

2022-10-28 17:04:30 浏览数 (1)

如何使用ASM给android的某个函数做插桩?

源码:https://github.com/shinecjj/AMStest

1、AMStest项目创建

直接在Android Studio中,new project 就行,等待项目第一次编译完成

2.gradle插件创建

在项目的根目录中,创建buildSrc文件夹,然后构建一下项目,然后在buildSrc文件夹中创建build.gradle配置文件,如下:

代码语言:javascript复制
plugins{
    //使用 java groovy 插件
    id 'java'
    id 'groovy'
}

group 'com.julive.sam'
version '0.0.1'

sourceCompatibility = 1.8

repositories{
    //使用阿里云的maven代理
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven { url 'https://maven.aliyun.com/repository/public' }
//    maven {
//        url 'http://maven.aliyun.com/nexus/content/groups/public/'
//    }
//    maven {
//        url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
//    }
}

def asmVersion = '8.0.1'

dependencies {
    //引入gradle api
    implementation gradleApi()
    implementation localGroovy()
    //引入android studio扩展gradle的相关api
    implementation "com.android.tools.build:gradle:4.1.0"
    //引入apache io
    implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
    //引入ASM相关api,这是我们插桩的关键,要靠他实现方法插桩
    implementation "org.ow2.asm:asm:$asmVersion"
    implementation "org.ow2.asm:asm-util:$asmVersion"
    implementation "org.ow2.asm:asm-commons:$asmVersion"
}

接下来创建插件代码目录,由于我们使用java写的插件,所以需要选中buildSrc,然后鼠标右键选择new,再选择directory,最后出现的对话框中选择 src/main/java,下图中是因为我的项目已经创建完了,所以只有groovy目录,如果你需要写groovy的实现就创建下图中文件夹路径,创建完这个下一步就是创建插件。

在java目录中,创建包名com.julive.sam,在该包路径下创建Plugins插件,代码如下:

代码语言:javascript复制
public class Plugins implements Plugin<Project> {
    @Override
    public void apply(Project project) {
            //registerTransform
            AppExtension android = project.getExtensions().getByType(AppExtension.class);
            android.registerTransform(new TransformTest());
        }
}

然后创建插件的配置resources文件夹,和java文件夹同级,在resources下创建文件夹META-INF/gradle-plugins/,最终在gradle-plugins中创建com.julive.sam.properties,意思是你的包名.properties ,一定要对应好包名,然后在该文件中加入代码

代码语言:javascript复制
implementation-class=com.julive.sam.Plugins

com.julive.sam.Plugins 你点击后,看能否跳转至 上面创建的Plugins插件中,如果可以直接跳转那就ok了。

3.下一步在App的build.gradle中配置插件

4.创建gradle的Transform实现

Transform是在.class -> .dex转换期间,用来修改.class文件的一套标准API,所以你现在应该知道了,在transform中我们肯定要调用ASM的实现,来实现.class文件的修改,最终转换为.dex文件。创建Transform的实现如下:

代码语言:javascript复制
public class TransformTest extends Transform {

    @Override
    public String getName() {
        // 随便起个名字
        return "TransformSam";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        //代表处理的 java 的 class 文件
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        //要处理所有的class字节码
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        // 是否增量编译,我们先不考虑,返回false
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        try {
            //待实现
            doTransform(transformInvocation); // hack
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

看上面注释是不是就对Transform有了一定的了解呢,那么如何处理.class文件呢?我们来实现doTransform函数,来看如何处理

代码语言:javascript复制
    private void doTransform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("doTransform   =======================================================");
        //inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        //删除之前的输出
        if (outputProvider != null)
            outputProvider.deleteAll();

        inputs.forEach(transformInput -> {
            //遍历directoryInputs
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(directoryInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getDirectoryInputs   ======================================================="   file.getName());
                    // 判断是.class文件
                    if (file.isFile() && file.getName().endsWith(".class")) {
                        try {
                            //ASM提供的读取类信息的对象
                            ClassReader classReader = new ClassReader(new FileInputStream(file));
                            //ASM提供的类修改对象,并将读到的信息交给classWriter
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                            //创建修改规则,TestClassVisitor
                            ClassVisitor visitor = new TestClassVisitor(classWriter);
                            //将修改规则给classReader
                            classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
                            //通过toByteArray方法,将变更后信息转成byte数组
                            byte[] bytes = classWriter.toByteArray();
                            //放入输出流中往原文件中写入
                            FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
                            fileOutputStream.write(bytes);
                            fileOutputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                if (outputProvider != null) {
                    File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                    try {
                        //将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
                        FileUtils.copyDirectory(directoryInput.getFile(), dest);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            //jarInputs
            transformInput.getJarInputs().forEach(jarInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(jarInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getJarInputs   ======================================================="   file.getName());
                });
                if (outputProvider != null) {
                    File dest = outputProvider.getContentLocation(
                            jarInput.getName(),
                            jarInput.getContentTypes(),
                            jarInput.getScopes(),
                            Format.JAR);
                    //将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
                    try {
                        FileUtils.copyFile(jarInput.getFile(), dest);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        });
    }

    //递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
    void getFileList(File file, ArrayList<File> fileList) {
        if (file.isFile()) {
            fileList.add(file);
        } else {
            File[] list = file.listFiles();
            for (File value : list) {
                getFileList(value, fileList);
            }
        }
    }

从transformInvocation的api中,我们获取了两个东西,一个是inputs,一个是outputProvider,我们遍历inputs后发现,他有两个api getDirectoryInputs和getJarInputs 这俩是什么东西呢?我描述不太好,我加了日志,来看下日志输出:

这下是不是看明白了,其实我对getDirectoryInputs做了一层文件筛选处理

代码语言:javascript复制
transformInput.getDirectoryInputs().forEach(directoryInput -> {
      ArrayList<File> list = new ArrayList<>();
      getFileList(directoryInput.getFile(), list);
});
  //递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
   void getFileList(File file, ArrayList<File> fileList) {
        if (file.isFile()) {
            fileList.add(file);
        } else {
            File[] list = file.listFiles();
            for (File value : list) {
                getFileList(value, fileList);
            }
        }
    }

好,从上面我们看出,已经找到了MainActivity的class文件,那么接下来给MainActivity.class的onCreate函数,插入两行代码,

5. 现在开始操作ASM的api

首先要实现ASM的 ClassVisitor 类来操作我们想要操作的类,它可以访问class文件的各个部分,比如方法、变量、注解等

基本的实现如下:

代码语言:javascript复制
public class TestClassVisitor extends ClassVisitor{

    private String className;
    private String superName;

    TestClassVisitor(final ClassVisitor classVisitor) {
        super(Opcodes.ASM6, classVisitor);
    }

    /**
     * 这里可以拿到关于.class的所有信息,比如当前类所实现的接口类表等
     *
     * @param version    表示jdk的版本
     * @param access     当前类的修饰符 (这个和ASM 和 java有些差异,比如public 在这里就是ACC_PUBLIC)
     * @param name       当前类名
     * @param signature  泛型信息
     * @param superName  当前类的父类
     * @param interfaces 当前类实现的接口列表
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        //委托函数
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        //找到我们需要修改的类,注意这里是/ 斜杠来表示文件的路径,并不是java代码中的.
        if (className.equals("com/julive/samtest/MainActivity")) {
            // 判断方法name是onCreate
            if (name.startsWith("onCreate")) {
                //插桩函数的实现,同样用到ASM提供的对象,下面看具体实现代码
                return new TestMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor, className, superName);
            }
        }
        return methodVisitor;
    }
}

这里集成AdviceAdapter,其实AdviceAdapter是继承自MethodVisitor,这是不是就跟ClassVisitor一一呼应呢,使用它是因为它比较方便的实现,提供了onMethodEnter,onMethodExit,正好是我们的需求。在onCreate的函数的前后各插入一行代码。但仔细看onMethodEnter的函数实现,你会发现一脸懵逼,不知道是啥玩意。往下看

代码语言:javascript复制
public class TestMethodVisitor extends AdviceAdapter {

    private String className;
    private String superName;

    protected TestMethodVisitor(int i, MethodVisitor methodVisitor, int i1, String s, String s1,String className,String superName) {
        super(i, methodVisitor, i1, s, s1);
        this.className = className;
        this.superName = superName;
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className   "---->"   superName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }

    @Override
    protected void onMethodExit(int opcode) {
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn("this is end");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
        super.onMethodExit(opcode);
    }
}

在这里推荐一个插件,https://plugins.jetbrains.com/plugin/14860-asm-bytecode-viewer-support-kotlin ,用插件测试代码如下:

代码语言:javascript复制
public class Test {
    void aa() {
        Log.i("TAG", "this is end");
    }
}

转换ASM代码如下:

代码语言:javascript复制
public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/julive/samtest/Test", null, "java/lang/Object", null);

        classWriter.visitSource("Test.java", null);

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(0, "aa", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(8, label0);
            methodVisitor.visitLdcInsn("TAG");
            methodVisitor.visitLdcInsn("this is end");
            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            methodVisitor.visitInsn(POP);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(9, label1);
            methodVisitor.visitInsn(RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label2, 0);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }

是不是很长,哈哈,这段代码其实是将整个Test类的东西,都通过ASM的方式生成,我们只需要找到对应的日志如下:

代码语言:javascript复制
   methodVisitor.visitLdcInsn("TAG");
   methodVisitor.visitLdcInsn("this is end");
   methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
   methodVisitor.visitInsn(POP);

然后将其放入到onMethodExit函数中,就可以了。

6.Tranfrom结合ASM实现

现在万事具备只欠东风,就是将Tranform拿到的class文件通过ASM做修改,具体如何关联,请看,回到刚才的doTransform中,改成如下代码:

代码语言:javascript复制
 private void doTransform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("doTransform   =======================================================");
        //inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        //删除之前的输出
        if (outputProvider != null)
            outputProvider.deleteAll();

        inputs.forEach(transformInput -> {
            //遍历directoryInputs
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(directoryInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getDirectoryInputs   ======================================================="   file.getName());
                    // 判断是.class文件
                    if (file.isFile() && file.getName().endsWith(".class")) {
                        try {
                            //ASM提供的读取类信息的对象
                            ClassReader classReader = new ClassReader(new FileInputStream(file));
                            //ASM提供的类修改对象,并将读到的信息交给classWriter
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                            //创建修改规则,TestClassVisitor
                            ClassVisitor visitor = new TestClassVisitor(classWriter);
                            //将修改规则给classReader
                            classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
                            //通过toByteArray方法,将变更后信息转成byte数组
                            byte[] bytes = classWriter.toByteArray();
                            //放入输出流中往原文件中写入
                            FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
                            fileOutputStream.write(bytes);
                            fileOutputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                if (outputProvider != null) {
                    File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                    try {
                        //将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
                        FileUtils.copyDirectory(directoryInput.getFile(), dest);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            //jarInputs
            transformInput.getJarInputs().forEach(jarInput -> {
                ArrayList<File> list = new ArrayList<>();
                getFileList(jarInput.getFile(), list);
                list.forEach(file -> {
                    System.out.println("getJarInputs   ======================================================="   file.getName());
                });
                if (outputProvider != null) {
                    File dest = outputProvider.getContentLocation(
                            jarInput.getName(),
                            jarInput.getContentTypes(),
                            jarInput.getScopes(),
                            Format.JAR);
                    //将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
                    try {
                        FileUtils.copyFile(jarInput.getFile(), dest);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        });
    }

    //递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
    void getFileList(File file, ArrayList<File> fileList) {
        if (file.isFile()) {
            fileList.add(file);
        } else {
            File[] list = file.listFiles();
            for (File value : list) {
                getFileList(value, fileList);
            }
        }
    }

7.反编译检查代码

好了,通过ASM的一顿操作,已经将代码插入到了MainActivity的onCreate函数中,我们如何验证?可以通过反编译来看,也可以通过日志,日志不太合理,因为一般我们不会插入很多日志来验证我们插入的正确性,太多了,照顾不过来,下面我们就反编译来看:这里推荐使用https://github.com/skylot/jadx 它提供了可视化操作,首先做如下操作:

代码语言:javascript复制
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist

执行成功后,可以执行如下:

代码语言:javascript复制
jadx-gui

然后就会打来工具,如下:

然后将 app的debug apk包拖到这个窗口就行,如下: 我们找到MainActivity如下:

而我们原代码是这样,跟我们预想的效果一致。

源码:https://github.com/shinecjj/AMStest

0 人点赞