前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ASM插桩举例

ASM插桩举例

作者头像
用户9854323
发布2022-10-28 17:04:30
9480
发布2022-10-28 17:04:30
举报
文章被收录于专栏:小陈飞砖

如何使用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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、AMStest项目创建
  • 2.gradle插件创建
  • 3.下一步在App的build.gradle中配置插件
  • 4.创建gradle的Transform实现
  • 5. 现在开始操作ASM的api
  • 6.Tranfrom结合ASM实现
  • 7.反编译检查代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档