ASM的tree api对匿名线程的hook操作详解

目录
  • 背景
  • ASM介绍
    • class文件
    • fields
    • methods
    • InsnList
    • Signature
  • 实战部分
    • 解决“匿名”Thread
  • 最后

背景

看完本章,你将会学习到用ASM的tree api进行对匿名线程的hook操作,同时也能够了解到asm相关的操作和背景知识介绍!对于ASM插桩来说,可能很多人都不陌生了,但是大多数可能都停留在core api上,对于现在市面上的一些插桩库,其实很多都用tree api进行编写了,因为tree api的简单与明了的特性,也越来越成为许多开源库的选择。(ASM有两套api类型,分别是core 和 tree)

ASM介绍

ASM其实就是一个可以编译字节码的工具,比如说我们日常开发会引入很多的类库对不对,又或者说我们的项目太大了,想修改某个点的时候,统一修改容易出错(比如隐私合规问题等),这个时候如果能有一个工具对生成后的class文件进行编辑的话,就非常方便我们进行后续的工作了。

本章主要介绍tree api,下文所说的ASM都是指tree api的操作哦,对于core api的介绍可以查看笔者曾经写过的文章Spider

class文件

我们常说的class文件,其实从二进制的角度出发,无非是分成以下几个部分:

可以看到,一个class文件其实就是由上图中的多个部分组成,而ASM,就是把这些结构进行了更进一步的抽象,对于class文件,其实就是抽象成asm中的class node类

对于一个class文件来说,通过以下就可以进行唯一性识别,分别是:version(版本),access(作用域,比如private等修饰符),name(名称),signature(泛型签名),superName(父类),interfaces(实现的接口),fields(当前的属性),methoss(当前的方法)。 所以如果想要修改一个class,我们修改对应的classNode即可

fields

属性,也是类非常重要的一部分,在字节码中,是如此定义的

对于一个属性,ASM将其抽象为FieldNode

对于一个属性field来说,通过以下就可以进行唯一性识别:access(作用域,跟class结构一样,比如private修饰),name(属性名称),desc(签名),signature(泛型签名),value(当前对应的数值)

methods

相比于属性,我们的方法结构更为复杂

相比于属性的单一,一个方法可能由多条指令组成而,一个方法的成功执行,也涉及到局部变量表跟操作数栈的配合。ASM中把方法抽象成这样一个定义 方法 = 方法头+方法体

方法头:即标识一个方法的基本属性,包括:access(作用域),name(方法名),desc(方法签名),signature(泛型签名),exceptions(方法可以抛出的异常)

方法体:相比于方法头,方法体的概念其实就比较简单了,其实方法体就是方法的各条指令的集合,主要包括instrutions(方法的指令集),tryCatchBlocks(异常的节点集),maxStack(操作数栈的最大深度),maxLocals(本地变量表的最大长度)

可以看到,方法其中的InsnList对象,是特指方法的指令集的抽象,这里继续讲解

InsnList

public class InsnList implements Iterable<AbstractInsnNode> {
    private int size;
    private AbstractInsnNode firstInsn;
    private AbstractInsnNode lastInsn;
    AbstractInsnNode[] cache;
    ...

可以看到,主要的对象就是firstInsn,与lastInsn,代表着方法指令集的头指令与尾指令,每一个指令其实都被抽象成了AbstractInsnNode的子类,AbstractInsnNode定义了一条指令最基础的信息,我们可以看看这个类的子类

这里我们再看看我们最常用的methodInsnNode

public class MethodInsnNode extends AbstractInsnNode {
  /**
   * The internal name of the method's owner class (see {@link
   * org.objectweb.asm.Type#getInternalName()}).
   *
   * <p>For methods of arrays, e.g., {@code clone()}, the array type descriptor.
   */
  public String owner;
  /** The method's name. */
  public String name;
  /** The method's descriptor (see {@link org.objectweb.asm.Type}). */
  public String desc;
  /** Whether the method's owner class if an interface. */
  public boolean itf;

这个就是一个普通方法指令最根本的定义了,owner(方法调用者),name(方法名称),desc(方法签名)等等,他们都有着相似的结构,这个也是我们接下来会实战的重点。

Signature

嗯!我们最后介绍一下这个神奇的东西!不知道大家在看介绍的时候,有没有一脸疑惑,这个我解释为泛型签名,这个跟desc(函数签名)参数有什么区别呢?当然,这个不仅仅在函数上有出现,在属性,类的结构上都有出现!是不是非常神奇!

其实Signature属性是在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性, 可以出现于类、属性表和方法表结构的属性表中。我们想想看,jdk1.5究竟是发生什么了!其实就是对泛型的支持,那么1.5版本之前的sdk怎么办,是不是也要进行兼容了!所以java标准组就想到了一个折中的方法,就是泛型擦除,泛型信息编译(类型变量、参数化类型)之后 都通通被擦除掉,以此来进行对前者的兼容。那么这又导致了一个问题,擦除的泛型信息有时候正是我们所需要的,所以Signature就出现了,把这些泛型信息存储在这里,以提供运行时反射等类型信息的获取!实际上可以看到,我们大部分的方法或者属性这个值都为null,只有存在泛型定义的时候,泛型的信息才会被存储在Signature里面

实战部分

好啦!有了理论基础,我们也该去实战一下,才不是口水文!以我们线程优化为例子,在工作项目中,或者在老项目中,可能存在大多数不规范的线程创建操作,比如直接new Thread等等,这样生成的线程名就会被赋予默认的名字,我们这里先把这类线程叫做“匿名线程”!当然!并不是说这个线程没有名字,而是线程名一般是“Thread -1 ”这种没有额外信息含量的名字,这样对我们后期的线程维护会带来很大的干扰,时间长了,可能就存在大多数这种匿名线程,有可能带来线程创建的oom crash!所以我们的目标是,给这些线程赋予“名字”,即调用者的名字

解决“匿名”Thread

为了达到这个目的,我们需要对thread的构造有一个了解,当然Thread的构造函数有很多,我们举几个例子

public Thread(String name) {
    init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}

可以看到,我们Thread的多个构造函数,最后一个参数都是name,即Thread的名称,所以我们的hook点是,能不能在Thread的构造过程,调用到有name的构造函数是不是就可以实现我们的目的了!我们再看一下普通的new Thread()字节码

那么我们怎么才能把new Thread()的方式变成 new Thread(name)的方式呢?很简单!只需要我们把init的这条指令变成有参的方式就可以了,怎么改变呢?其实就是改变desc!方法签名即可,因为一个方法的调用,就是依据方法签名进行匹配的。我们在函数后面添加一个string的参数即可

node是methidInsnNode
def desc =
        "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
node.desc = desc

那么这样我们就可以完成了吗,非也非也,我们只是给方法签名对加了一个参数,但是这并不代表我们函数就是这么运行的!因为方法参数的参数列表中的string参数我们还没放入操作数栈呢!那么我们就可以构造一个string参数放入操作数栈中,这个指令就是ldc指令啦!asm为我们提供了一个类是LdcInsnNode,我们可以创建一个该类对象即可,构造参数需要传入一个字符串,那么这个就可以把当前方法的owner(解释如上,调用者名称)放进去了,是不是就达到我们想要的目的了!好啦!东西我们又了,我们要在哪里插入呢?

所以我们的目标很明确,就是在init指令调用前插入即可,asm也提供了insertBefore方法,提供在某个指令前插入的便捷操作。

method.instructions.insertBefore(
        node,
        new LdcInsnNode(klass.name)
)

我们看看最后插入后的字节码

当然,我们插入asm代码一般是在android提供给我们的Transform阶段进行的(agp新版有改变,但是大体工作流程一致),所以我们在transfrom中为了避免对类的过度干扰,我们还需要把不必要的阶段提早剔除!比如我们只在new Thread操作,那么就把非Opcodes.INVOKESPECIAL的操作过滤即可。还有就是非init阶段(即非构造函数阶段)或者owner不为Thread类就可以提前过滤,不参与更改即可。

那我们看到完整的代码(需要在Transform中执行的代码)

static void transform(ClassNode klass) {
    println("ThreadTransformUtils")
    // 这里只处理Thread
    klass.methods?.forEach { methodNode ->
        methodNode.instructions.each {
            // 如果是构造函数才继续进行
            if (it.opcode == Opcodes.INVOKESPECIAL) {
                transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
            }
        }
    }
}
private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
    // 如果不是构造函数,就直接退出
    if (node.name != "<init>" || node.owner != THREAD) {
        return
    }
    println("transformInvokeSpecial")
    transformThreadInvokeSpecial(node, klass, method)
}
private static void transformThreadInvokeSpecial(
        MethodInsnNode node,
        ClassNode klass,
        MethodNode method
) {
    switch (node.desc) {
    // Thread()
        case "()V":
            // Thread(Runnable)
        case "(Ljava/lang/Runnable;)V":
            method.instructions.insertBefore(
                    node,
                    new LdcInsnNode(klass.name)
            )
            def r = node.desc.lastIndexOf(')')
            def desc =
                    "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
            // println(" + $SHADOW_THREAD.makeThreadName(Ljava/lang/String;Ljava/lang/String;) => ${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
            println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
            node.desc = desc
            break
    }
}

最后

看到这里,应该可以了解到asm tree api相关用法与实战了,希望能有所帮助!

以上就是ASM的tree api对匿名线程的hook操作详解的详细内容,更多关于ASM tree api匿名线程hook的资料请关注我们其它相关文章!

时间: 2022-09-17

Android下hook点击事件的示例

Hook是一种思想,也就是将原来的事件,替换到我们自己的事件,方便我们做一些切入处理.目的是不修改原来的代码,同时也避免遗漏的N多类里面处理. 最近需要在现有的app中设置统计埋点.去业务代码里埋的话似乎耦合度太高.所以决定使用hook的方法对事件进行埋点处理. 这里先记一下对点击事件hook的基本流程. 1.先建一个代理类实现View.OnClickListener,用来做点击后的后续处理. import android.view.View; /** * 实现点击监听 */ public cl

Android开发AsmClassVisitorFactory使用详解

目录 前言 AsmClassVisitorFactory 新的Extension 实战 ClassVisitor 实际代码分析 个人观点 前言 之前就和大家介绍过AGP(Android Gradle Plugin) 7.0.0版本之后Transform 已经过期即将废弃的事情.而且也简单的介绍了替换的方式是Transform Action,经过我这一阵子的学习和调研,发现只能说答对了一半吧.下面介绍个新东西AsmClassVisitorFactory. com.android.build.api

Android手机屏幕同步工具asm.jar

有时候可能需要将手机上的一些操作投影出来,比如一些App Demo的展示等.其实,有专门的硬件设备能干这件事儿,但没必要专门为展示个Demo去花钱买硬件设备.正好,对于Android系统的手机,有一个开源的jar包能干这事儿:Android Screen Monitor(asm.jar) 步骤: 一 . 下载附件 asm.jar 官网 https://code.google.com/p/android-screen-monitor/ 二. 放到任意目录下, 这里放到D盘根目录; 将asm.jar

Android性能优化之plt hook与native线程监控详解

目录 背景 native 线程创建 PLT PLT Hook xhook bhook plt hook总结 背景 我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有

Android ASM插桩探索实战详情

目录 前言 ASM的作用是什么? 如何使用ASM? 基本使用方式 自定义ClassVisitor ASM ByteCode Viewer 如何将ASM运用都我们的实际项目中来? 引入工程 Android Gradle Plugin 创建插件项目 配置插件 实现插件 发布插件 应用插件 Android Transform 方法节流 方法耗时日志 如何调试 发布线上的额外工作 插件项目的maven仓库 编译影响评估 Tips 总结 前言 我们都知道,在Android编译过程中,Java代码会被编译成

android的got表HOOK实现代码

概述 对于android的so文件的hook根据ELF文件特性分为:Got表hook.Sym表hook和inline hook等. 全局符号表(GOT表)hook,它是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数. Androd so注入和函数Hook(基于got表)的步骤: 1.ptrace附加目标pid进程; 2.在目标pid进程中,查找内存空间(用于存放被注入的so文件的路径和so中被调用的函

Android okhttputils现在进度显示实例代码

OkHttpUtils是一款封装了okhttp的网络框架,支持大文件上传下载,上传进度回调,下载进度回调,表单上传(多文件和多参数一起上传),链式调用,整合Gson,自动解析返回对象,支持Https和自签名证书,支持cookie自动管理,扩展了统一的上传管理和下载管理功能. //download the new app private void downLoadNewApp(NewVersion.XianzaishiRfBean version) { if (StringUtils.isEmpt

Angular4表单验证代码详解

 背景: 最近在itoo页面调整的时候,发现页面表单或者是文本框没有做基本的判断操作,所以着手demo一篇,希望对大家有帮助!! -------------------------------------------------------------------------------- 1.创建表单组件: ng g c login1 2.1单规则验证: <label>用户名:</label> <input type="text" #userNameRe

Vue表单实例代码

什么是 Vue.js? Vue.js 是用于构建交互式的 Web 界面的库. Vue.js 提供了 MVVM 数据绑定和一个可组合的组件系统,具有简单.灵活的 API. Vue.js 特点 简洁: HTML 模板 + JSON 数据,再创建一个 Vue 实例,就这么简单. 数据驱动: 自动追踪依赖的模板表达式和计算属性. 组件化: 用解耦.可复用的组件来构造界面. 轻量: ~24kb min+gzip,无依赖. 快速: 精确有效的异步批量 DOM 更新. 模块友好: 通过 NPM 或 Bower

Android手机屏幕敲击解锁功能代码

1.前言 现在市面上有不少Android手机支持敲击屏幕解锁,敲击屏幕解锁是一项很实用的功能,但一来只支持敲击屏幕,二来只能用于解锁或锁屏,再者我们应用层的开发者切不进去,完全无法玩起来.开发者,开发者,我们既然身为开发者何不搞点大新闻,那么这次我来教教各位如何用代码来实现手机的敲击识别,听起来是不是很有趣,有些跃跃欲试呢.事实上在ios上已经有实现这个功能的应用:Knock,一款敲击来解锁Mac电脑的应用,售价4.99美元,约为33人民币.有时候真想去做ios开发,可以开心的为自己的应用定价,

spring security数据库表结构实例代码

PD建模图 建模语句 alter table SYS_AUTHORITIES_RESOURCES drop constraint FK_SYS_AUTH_REFERENCE_SYS_AUTH; alter table SYS_AUTHORITIES_RESOURCES drop constraint FK_SYS_AUTH_REFERENCE_SYS_RESO; alter table SYS_RESOURCES drop constraint FK_SYS_RESO_REFERENCE_SYS

Android 手势 正则匹配图片实例代码

为没有手势的控件(ViewFlipper) 添加手势 xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools

Android Studio多渠道批量打包及代码混淆

一.批量打包 1.集成了友盟统计,并在AndroidManifest.xml中添加了如下代码 <meta-data android:name="UMENG_CHANNEL" android:value="${CHANNEL_VALUE}"/> 2.在app的build.gradle的android标签下添加如下代码: productFlavors { myapp {} _360 {} appchina {} hiapk {} } productFlavo

C语言实现动态顺序表的实现代码

C语言实现动态顺序表的实现代码 顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构.线性表采用顺序存储的方式存储就称之为顺序表.顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中. 静态实现:结构体内部只需两个成员,其中一个为固定大小(MAX)的数组,用来存放我们的数据.数组大小我们可以通过在头文件中改变MAX的值来改变. 动态实现:在内存中开辟一块空间,可以随我们数据数量的增多来扩容. 来看看动态的顺序表实现: 1.seqli

C语言顺序表的实现代码

本文实例为大家分享了C语言实现顺序表的具体代码,供大家参考,具体内容如下 seqlist.h #ifndef __SEQLIST_H__ #define __SEQLIST_H__ #include<cstdio> #include<malloc.h> #include<assert.h> #define SEQLIST_INIT_SIZE 8 #define INC_SIZE 3 //空间增量的大小 typedef int ElemType; typedef stru

Android 以任意比例裁剪图片代码分享

公司的一个小伙伴写的,可以按照任意比例裁剪图片.我觉得挺好用的.简单在这里记录一下,以后肯定还会用到. public class SeniorCropImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener, View.OnLayoutChangeListener { /* For drawing color field start */ private static final int