自己实现字节码增强

涉及技术:

JVMTI ,javaagent,Attach API, Instrument ,Javassist

JVMTI:

JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套对 JVM 进行操作的工具接口。通过JVMTI,可以实现对 JVM 的多种操作,它通过接口注册 各种事件勾子,在 JVM 事件触发时,同时触发预定义的勾子,以实现对各个 JVM 事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退 出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等 待、VM 启动与退出等等。

javaagent

Agent 就是 JVMTI 的一种实现,Agent 有两种启动方式,一是随 Java 进 程启动而启动;二是运行时载入,通过 attach API,将模块(jar 包)动态地 Attach 到指定进程 id 的 Java 进程内。

Attach API

Attach API 的作用是提供 JVM 进程间通信的能力,比如说我们为了让另外一 个 JVM 进程把线上服务的线程 Dump 出来,会运行 jstack 或 jmap 的进程,并传 递 pid 的参数,告诉它要对哪个进程进行线程 Dump,这就是 Attach API 做的事情。

Instrument

instrument 是 JVM 提供的一个可以修改已加载类的类库,专门为 Java 语言编 写的插桩服务提供支持。它需要依赖 JVMTI的 Attach API 机制实现。

Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

实现

因为javaagent有两种实现方式,但是我们想要实现的是在不停机的情况下进行字节码增强,所以选择使用Attach API进行动态载入。使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Agent-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
    在这里插入图片描述
  2. 创建一个Agent-Class 指定的类,类中包含 agentmain 方法,方法逻辑由用户自己确定。
public class AgentMainTest{
	public static void agentmain( String agentArgs, Instrumentation instrumentation ){
		// 指定我们自己定义的 Transformer,在其中利用 Javassist 做字节码替换
		instrumentation.addTransformer( new DefineTransformer(), true );
		try{
			instrumentation.retransformClasses( Base.class );
			System.out.println( "Agent Load Done." );
		}
		catch( UnmodifiableClassException e ){
			System.out.println( "agent load failed!" );
		}
	}
}
public class DefineTransformer implements ClassFileTransformer{
	
	// 类文件加载的时候调用
	@Override
	public byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
			byte[] classfileBuffer ){

		try{
			ClassPool cp = ClassPool.getDefault();
			CtClass cc = cp.get( "com.bj58.jvmti.Base" );
			CtMethod m = cc.getDeclaredMethod( "run" );
			// 解冻class,以便修改class内容
			cc.defrost();
			m.insertBefore( "{ System.out.println(\"run() start\"); }" );
			m.insertAfter( "{ System.out.println(\"run() end\"); }" );
			return cc.toBytecode();
		}
		catch( Exception e ){
			e.printStackTrace();
		}
		return null;
	}

}
  1. 将 agentmain 的类和 MANIFEST.MF 文件打成 jar 包。
  2. 最后利用 Attach API,将我们打包好的 jar 包 Attach 到指定的 JVM pid 上代码如下
public class TestAgentMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{
        System.out.println("running JVM start ");
        // 使用attach api, Attach到目标 JVM 上,建立通信管道
        VirtualMachine vm = VirtualMachine.attach("15219");
        // 让目标JVM加载Agent
        vm.loadAgent("/Users/benettchen/Desktop/javaagent.jar");
    }
}

Base类

public class Base{

	public static void main( String[] args ){

		System.out.println( "pid:" + populateProcessId() );
		while( true ){
			try{
				Thread.sleep( 3000L );
			}
			catch( Exception e ){
				break;
			}
			run();
		}
	}

	private static void run(){

		System.out.println( "working..." );
	}

	private static String populateProcessId(){
		/*
		 * runtimeMXBean.getName()取得的值包括两个部分:PID和hostname,两者用@连接。
		 */
		RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
		return runtimeMXBean.getName().split( "@" )[ 0 ];
	}
}

运行流程梳理:

  1. 首先编写agent项目,并其打包成jar包。
  2. 启动Base这个Java进程,获取该进程id。
  3. 启动TestAgentMain,将jar包动态的attach到目标jvm(Base进程)上
  4. 在attach后,由于在MANIFEST.MF 中指定了 Agent-Class,所以会走到AgentMainTest的agentmain方法。
  5. 使用javassist修改Base类的字节码,然后利用Instrumentation完成类的重新加载。

以下为运行时重新载入jar的效果:
在这里插入图片描述
从运行结果可以看出,在载入jar包后,在run方法前后分别输出了“run() start”和“run() end”,实现了在运行时字节码增强并重新载入到jvm的目的。

闲话:
自己动手实现完对Java运行程序字节码动态增强后,突然感觉可以利用这个技术对Java程序开挂,哈哈。想到自己的idea是Java语言编写的,而且也是通过jar包进行破解的,就想看看它的实现是否是按照这个思路。于是乎兴奋的将破解的jar包进行反编译,发现了惊奇的一幕:
在这里插入图片描述
在这里插入图片描述
它用的正是javaagent的第一种实现,在JVM启动时进行加载,所以在使用这个jar包后,需要重启idea。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章