注册

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

前言


之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的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…


作者:陈平大将
链接:https://juejin.cn/post/7021033161488334856
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册