自己實現字節碼增強

涉及技術:

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。

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