使用BlackHook(黑钩) 可以Hook一切java或者kotlin方法

11,340

前言

之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,为了实现在线上监控线程的使用,于是我便开发了BlackHook插件,也可以hook一切java方法,而且很稳定,没有兼容性问题,真是十足的黑科技

简介

BlackHook 是一个实现编译时插桩的gradle插件,基于ASM+Tranfrom实现,理论上可以hook任意一个java方法或者kotlin方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到,就可以使用ASM在代码对应的字节码处插入特定字节码,从而hook该方法

优点

  1. 用DSL(领域特定语言)使用该插件,使用简单,配置灵活,而且插入的字节码可以使用 ASM Bytecode Viewer Support Kotlin 插件自动生成,上手难度低
  2. 理论上可以hook任意一个java方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到
  3. 基于ASM+Tranfrom实现,在编译阶段直接修改字节码,效率高,没有兼容性问题

使用

在app下面的build.gradle文件添加如下代码

apply plugin: 'com.blackHook'

/**
 * 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
 * ThreadCheck类的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
 * ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
 */
void createHookThreadByteCode(MethodVisitor mv, String className) {
    mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
    mv.visitInsn(Opcodes.DUP)
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
    mv.visitLdcInsn(className)
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

/**
 * 返回需要被hook的方法,需要被hook的方法是Thread的构造函数
 */
List<HookMethod> getHookMethods() {
    List<HookMethod> hookMethodList = new ArrayList<>()
    hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "java/lang/Thread") }))
    return hookMethodList
}

blackHook {
    //表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
    inputTypes BlackHook.CONTENT_CLASS
    //表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
    scopes BlackHook.SCOPE_FULL_PROJECT
    //表示是否支持增量编译,false不支持
    isIncremental false
    //表示hook的方法
    hookMethodList = getHookMethods()
}

以上的代码其实是hook的Thread的构造函数,将ThreadCheck的printThread方法hook到了Thread的构造函数中,每次调用线程的构造函数的时候就会调用ThreadCheck的printThread方法,这个方法会打印出Thread的构造函数的调用堆栈,从而可以在控制台知道哪个页面的哪行代码实例化了Thread,ThreadCheck的代码如下

class ThreadCheck {

    var isCanAppendLog = false
    private val tag = "====>ThreadCheck"

    fun printThread(name : String){

        println("====>printThread:${name}")

        val es = Thread.currentThread().stackTrace

        val normalInfo = StringBuilder(" \nThreadTrace:")
            .append("\nthreadName:${name}")
            .append("\n====================================threadTraceStart=======================================")

        for (e in es) {

            if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
                isCanAppendLog = false
            }

            if (e.className.contains("ThreadCheck") && e.methodName == "printThread") {
                isCanAppendLog = true
            } else {
                if (isCanAppendLog) {
                    normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})")
                }
            }
        }
        normalInfo.append("\n=====================================threadTraceEnd=======================================")

        Log.i(tag, normalInfo.toString())
    }

}

上面的代码获取了调用堆栈,并且打印到控制台

实现原理

首先它是一个gradle 的自定义Plugin,其次它是通过在编译阶段修改字节码实现Hook,在编译阶段通过Tranfrom扫描所有的字节码,然后根据在使用插件的时候设置的需要被Hook的方法,插入需要被插入的字节码, 需要被插入的字节码也是在使用的时候设置的,例如下面的代码

/**
 * 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
 * ThreadCheck的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
 * ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
 */
void createHookThreadByteCode(MethodVisitor mv, String className) {
    mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
    mv.visitInsn(Opcodes.DUP)
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
    mv.visitLdcInsn(className)
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

准备过程

实现这个gradle插件需要我们有足够的预备知识,如下:

实现过程

1.自定义gradle plugin

因为这是一个gradle插件,所以需要我们自定义一个gradle的plugin

1. 新建一个模块

在工程中新建一个模块,命名为"buildSrc",注意,一定要命名为buildSrc,否则在工程中必须要将代码发布到本地或者远程maven仓库中才能正常使用,这样调试不方便,如下所示:

image.png

2. 然后配置gradle脚本,代码如下所示:

plugins {
    id 'java-library'
    id 'maven'
    id 'groovy'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
    implementation gradleApi()//gradle sdk
    implementation localGroovy()
    implementation "com.android.tools.build:gradle:3.4.1"
    implementation 'org.ow2.asm:asm:9.1'
    implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 实现Plugin类

新建groovy文件夹,新建BlackHookPlugin类,继承Transform类,实现Plugin接口

image.png

BlackHookPlugin代码如下所示:

package com.blackHook.plugin

class BlackHookPlugin extends Transform implements Plugin<Project> {

    ....此处省略了很多代码

    @Override
    void apply(Project target) {
        println("注册了")
        project = target
        target.extensions.getByType(BaseExtension).registerTransform(this)
        target.extensions.create("blackHook", BlackHook.class)
    }
    
     ....此处省略了很多代码
}

新建resources文件夹,新建com.blackHook.properties文件,如下所示

image.png

com.blackHook.properties文件的代码如下:

implementation-class=com.blackHook.plugin.BlackHookPlugin

implementation-class的值即是BlackHookPlugin的完整路径,另外,com.blackHook.properties文件的文件名既是使用插件的时候的插件名,如下代码:

apply plugin: 'com.blackHook'

2. 实现BlackHook扩展类

新建BlackHook类,代码如下

public class BlackHook {

    Closure methodHooker;

    List<HookMethod> hookMethodList = new ArrayList<>();

    public static final String CONTENT_CLASS = "CONTENT_CLASS";
    public static final String CONTENT_JARS = "CONTENT_JARS";
    public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES";

    public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT";
    public static final String PROJECT_ONLY = "PROJECT_ONLY";

    String inputTypes = CONTENT_CLASS;

    String scopes = SCOPE_FULL_PROJECT;

    boolean isNeedLog = false;

    boolean isIncremental = false;

    public Closure getMethodHooker() {
        return methodHooker;
    }

    public void setMethodHooker(Closure methodHooker) {
        this.methodHooker = methodHooker;
    }

    public List<HookMethod> getHookMethodList() {
        return hookMethodList;
    }

    public void setHookMethodList(List<HookMethod> hookMethodList) {
        this.hookMethodList = hookMethodList;
    }

    public String getInputTypes() {
        return inputTypes;
    }

    public void setInputTypes(String inputTypes) {
        this.inputTypes = inputTypes;
    }

    public String getScopes() {
        return scopes;
    }

    public void setScopes(String scopes) {
        this.scopes = scopes;
    }

    public boolean getIsIncremental() {
        return isIncremental;
    }

    public void setIsIncremental(boolean incremental) {
        isIncremental = incremental;
    }

    public boolean getIsNeedLog() {
        return isNeedLog;
    }

    public void setIsNeedLog(boolean needLog) {
        isNeedLog = needLog;
    }
}

这个类用于接收开发人员使用插件的时候设置的参数和需要被Hook的方法以及参与Hook的字节码,我们在使用blackHook插件的时候可以使用DSL的方式来使用,如下代码所示:

blackHook {
    //表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录), RESOURCES 表示要处理的是标准的 java 资源
    inputTypes BlackHook.CONTENT_CLASS
    //表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
    scopes BlackHook.SCOPE_FULL_PROJECT
    //表示是否支持增量编译,false不支持
    isIncremental false
    //表示hook的方法
    hookMethodList = getHookMethods()
}

之所以可以这么做是因为我们在BlackHookPlugin将BlackHook类添加到了target.extensions(扩展属性)中, 如下代码:

class BlackHookPlugin extends Transform implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.extensions.create("blackHook", BlackHook.class)
    }
}

3.开始实现扫描

需要在BlackHookPlugin的transform()方法中扫描全局代码,代码如下:

  @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        if (blackHook == null) {
            blackHook = new BlackHook()
            blackHook.methodHooker = project.extensions.blackHook.methodHooker
            blackHook.isNeedLog = project.extensions.blackHook.isNeedLog
            for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) {
                HookMethod hookMethod = new HookMethod()
                hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className
                hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName
                hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor
                hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode
                blackHook.hookMethodList.add(hookMethod)
            }
        }
        inputs.each { input ->
            input.directoryInputs.each { directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍历jarInputs
            input.jarInputs.each { JarInput jarInput ->
                //处理jarInputs
                handleJarInputs(jarInput, outputProvider)
            }
        }
        super.transform(transformInvocation)
    }

    void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { file ->
                String name = file.name
                if (name.endsWith(".class") && !name.startsWith("R$drawable")
                        && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook)
                    classReader.accept(classVisitor, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }

        //处理完输入文件之后,要把输出给下一个任务
        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes,
                Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name

            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插桩class
                if (entryName.endsWith(".class") && !entryName.startsWith("R$")
                        && !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
                    //class文件处理
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new AllClassVisitor(classWriter, blackHook)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //结束
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

扫描的过程中会将扫描到的所有类的信息(包含类名,父类名,方法名等)交给AllClassVisitor类,AllClassVisitor类代码如下所示:

public class AllClassVisitor extends ClassVisitor {
    private String className;
    private BlackHook blackHook;
    private String superClassName;

    public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) {
        super(ASM6, classVisitor);
        this.blackHook = blackHook;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        className = name;
        superClassName = superName;
    }

    // 扫描到每个类中的方法的时候会回调到这个方法
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        // 新建AllMethodVisitor类,将扫描到类和方法的信息以及BlackHook类存储的参数交给    AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法
        return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
    }

然后在AllClassVisitor类中会将将扫描到的类和方法的信息以及BlackHook扩展类存储的参数交给AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法,AllMethodVisitor代码如下:

class AllMethodVisitor extends AdviceAdapter {
    private final String methodName;
    private final String className;
    private BlackHook blackHook;
    private String superClassName;

    protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) {
        super(ASM5, methodVisitor, access, name, descriptor);
        this.blackHook = blackHook;
        this.methodName = name;
        this.className = className;
        this.superClassName = superClassName;
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
        super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
        if (blackHook.isNeedLog) {
            System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor);
        }
        if (blackHook != null && blackHook.hookMethodList != null && blackHook.hookMethodList.size() > 0) {
            for (int i = 0; i < blackHook.hookMethodList.size(); i++) {
                HookMethod hookMethod = blackHook.hookMethodList.get(i);
                //这里根据开发人员设置的需要hook的方法以及扫描到的方法来判断是否需要hook
                if ((owner.equals(hookMethod.className) || superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) {
                    hookMethod.createBytecode.call(mv);
                    break;
                }
            }
        }
    }
}

在这个类中根据开发人员调用插件的时候设置的需要hook的方法以及扫描到的方法来判断是否需要hook

4.源码

github.com/18824863285…