JDK 1.5 開始,Java新增了 Instrumentation
( Java Agent API )和 JVMTI
( JVM Tool Interface )功能,允許JVM在載入某個 class 檔案之前對其位元組碼進行修改,同時也支援對已載入的 class (類位元組碼)進行重新載入( Retransform )。
在1.6版本新增了attach(附加方式),可以對執行中的Java程序插入Agent,instrumentation
包被賦予了更強大的功能:啟動後的 instrument、原生程式碼(native code)instrument,以及動態改變 classpath 等等。這些改變,意味著 Java 具有了更強的動態控制、解釋能力,它使得 Java 語言變得更加靈活多變。
java.lang.instrument
包的具體實現,依賴於 JVMTI
。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機器器提供的,為 JVM 相關的工具提供的本地程式設計介面集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套」代理」程式機制,可以支援第三方工具程式以代理的方式連線和存取 JVM,並利用 JVMTI 提供的豐富的程式設計介面,完成很多跟 JVM 相關的功能
Java Agent可以去實現位元組碼插樁、動態跟蹤分析等,比如RASP產品和Java Agent記憶體馬。
java.lang.instrument
包位於rt.jar
共包含如下類和介面:
此異常為非法的位元組碼格式化異常,由ClassFileTransformer.transform
的實現丟擲。
丟擲此異常的原因是由於初始類檔案位元組無效,或者由於以前應用的轉換損壞了位元組碼。
當程式無法修改制定的類時,會丟擲該異常。由 Instrumentation.redefineClasses
的實現丟擲。
public final class ClassDefinition {
/**
* 要重定義的類
*/
private final Class<?> mClass;
/**
* 用於替換的本地 class ,為 byte 陣列
*/
private final byte[] mClassFile;
/**
* 構造方法,使用提供的類和類檔案位元組建立一個新的 ClassDefinition 繫結
*/
public ClassDefinition( Class<?> theClass, byte[] theClassFile) {
if (theClass == null || theClassFile == null) {
throw new NullPointerException();
}
mClass = theClass;
mClassFile = theClassFile;
}
/**
* 以下為 getter 方法
*/
public Class<?> getDefinitionClass() {
return mClass;
}
public byte[] getDefinitionClassFile() {
return mClassFile;
}
}
此介面為轉換類檔案的代理介面。提供了 transform()
方法用於修改原類的注入。
我們可以在獲取到 Instrumentation
物件後通過 addTransformer()
方法新增自定義類檔案轉換器。
public interface ClassFileTransformer {
/**
* 類檔案轉換方法,重寫transform方法可獲取到待載入的類相關資訊
*
* @param loader 定義要轉換的類載入器;如果是引導載入器,則為 null
* @param className 類名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類載入,則為 null
* @param protectionDomain 要定義或重定義的類的保護域
* @param classfileBuffer 類檔案格式的輸入位元組緩衝區(不得修改)
* @return 返回一個通過ASM修改後新增了防禦程式碼的位元組碼byte陣列。
*/
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
重寫 transform() 方法需要注意以下事項:
java/lang/String
而不是我們常用的類名方式:java.lang.String
。 Can-Redefine-Classes: true
而且手動呼叫了retransformClasses()
方法也一樣無法retransform。在以下三種情形下 ClassFileTransformer.transform()
會被執行:
java.lang.instrument.Instrumentation
是 Java 提供的監測執行在 JVM 程式的 API 。利用 Instrumentation 我們可以實現如下功能:
類方法 | 功能 |
---|---|
void addTransformer(ClassFileTransformer transformer, boolean canRetransform) |
新增一個 Transformer,是否允許 reTransformer |
void addTransformer(ClassFileTransformer transformer) |
新增一個 Transformer |
boolean removeTransformer(ClassFileTransformer transformer) |
移除一個 Transformer |
boolean isRetransformClassesSupported() |
檢測是否允許 reTransformer |
void retransformClasses(Class<?>... classes) |
過載入(retransform)類 |
boolean isModifiableClass(Class<?> theClass) |
確定一個類是否可以被 retransformation 或 redefinition 修改 |
Class[] getAllLoadedClasses() |
獲取 JVM 當前載入的所有類 |
Class[] getInitiatedClasses(ClassLoader loader) |
獲取指定類載入器下所有已經初始化的類 |
long getObjectSize(Object objectToSize) |
返回指定物件大小 |
void appendToBootstrapClassLoaderSearch(JarFile jarfile) |
新增到 BootstrapClassLoader 搜尋 |
void appendToSystemClassLoaderSearch(JarFile jarfile) |
新增到 SystemClassLoader 搜尋 |
boolean isNativeMethodPrefixSupported() |
是否支援設定 native 方法 Prefix |
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix) |
通過允許重試,將字首應用到名稱,此方法修改本機方法解析的失敗處理 |
boolean isRedefineClassesSupported() |
是否支援類 redefine |
void redefineClasses(ClassDefinition... definitions) |
重定義(redefine)類 |
這部分由於參考作者 throwable 總結較好,直接參照。
instrument 的底層實現依賴於 JVMTI ,也就是 JVM Tool Interface ,它是 JVM 暴露出來的一些供使用者擴充套件的介面集合, JVMTI 是基於事件驅動的, JVM 每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者去擴充套件自己的邏輯。 JVMTIAgent 是一個利用 JVMTI 暴露出來的介面提供了代理啟動時載入(agent on load)、代理通過 attach 形式載入(agent on attach)和代理解除安裝(agent on unload)功能的動態庫。而 instrument agent 可以理解為一類 JVMTIAgent 動態庫,別名是 JPLISAgent (Java Programming Language Instrumentation Services Agent),也就是專門為 Java 語言編寫的插樁服務提供支援的代理
Java agent的使用方式有兩種:
實現premain
方法,在JVM啟動前載入。
實現agentmain
方法,在JVM啟動後載入
premain
和agentmain
函數宣告如下,方法名相同情況下,擁有Instrumentation inst
引數的方法優先順序更高:
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
JVM 會優先載入帶 Instrumentation
簽名的方法,載入成功忽略第二種;如果第一種沒有,則載入第二種方法。
第一個引數String agentArgs
就是Java agent的引數。
Inst
是一個 java.lang.instrument.Instrumentation
的範例,可以用來類定義的轉換和操作等等。
JVM啟動時 會先執行 premain 方法,大部分類載入都會通過該方法,注意:是大部分,不是所有。遺漏的主要是系統類,因為很多系統類先於 agent 執行,而使用者類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,就可以結合第三方的位元組碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。
大致分為以下邏輯:
premain
方法並呼叫Instrumentation#addTransformer
方法新增一個自定義的Transformer
Transformer
類,實現Instrumentation
介面,在transform
方法中寫入自己想要的AOP邏輯-javaagent
並指定我們打包好的jar這裡需要2個專案,1個為javaagent的jar包,另1個為被javaagent代理的類。最終在被代理類的main方法執行前先執行我們Agent中的premain方法
編寫javaagent
相關程式碼
建立一個Maven
專案,其中建立一個Premain
類,裡面需要包含premain
方法
package org.gk0d;
import java.lang.instrument.Instrumentation;
public class Premain {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("agentArgs"+agentArgs);
inst.addTransformer(new DefineTransformer(),true);//呼叫addTransformer新增一個Transformer
}
}
建立DefineTransformer
類,實現ClassFileTransformer
介面
package org.gk0d;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load class"+className); //列印載入的類
return new byte[0];
}
}
建立MANIFEST.MF檔案
此時專案如果打成jar包,缺少入口main檔案,所以需要自己定義一個MANIFEST.MF
檔案,用於指明premain的入口在哪裡:
手動建立的話需要在resources/META-INF
目錄下建立MANIFEST.M
F檔案,內容如下:注意多留一行空行
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.gk0d.Premain
或者通過pom.xml中呼叫Maven的外掛去建立該檔案
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動新增META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.gk0d.Premain</Premain-Class>
<Agent-Class>org.gk0d.Premain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
以下是MANIFEST.MF的其他選項
Premain-Class :包含 premain 方法的類(類的全路徑名)
Agent-Class :包含 agentmain 方法的類(類的全路徑名)
Boot-Class-Path :設定引導類載入器搜尋的路徑列表。查詢類的特定於平臺的機制失敗後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件語法。如果該路徑以斜槓字元(「/」)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之後某一時刻啟動的,則忽略不表示 JAR 檔案的路徑。(可選)
Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)
Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)
Can-Set-Native-Method-Prefix: true表示能設定此代理所需的本機方法字首,預設值為 false(可選)
打包
設定完後然後使用 maven 構建agent jar 包:mvn clean install
。
編寫測試類
為了更直觀,起了個新專案
public class Test {
public static void main(String[] args) {
System.out.println("main Method");
}
}
-javaagent模式啟動
JVM啟動引數新增,這裡使用絕對路徑,否則容易出問題
-javaagent:D:/vul/agent/target/agent-1.0-SNAPSHOT.jar
執行main方法之前會載入所有的類,包括系統類和自定義類。而在ClassFileTransformer
中會去攔截系統類和自己實現的類物件,邏輯則是在ClassFileTransformer
實現類的transform
方法中定義。
而在這裡transform
感覺是類似於一個filter
會去攔截/遍歷一些要在JVM中載入的類,而在transform
方法中我們可以定義一些邏輯,比如if className== xxx
時走入一個邏輯去實現AOP。而其中就可以利用如javassist
技術修改位元組碼並作為transform
方法的返回值,這樣就在該類在JVM中載入前(-javaagent模式)修改了位元組碼
這裡在之前Test
類中新新增一個方法,並在Agent
裡我們自定義的Transformert
中transform
新增一個邏輯,使用javassist
去修改我們Test類中新新增的方法
Test類中新加一個call方法
public class Test {
public static void main(String[] args) {
System.out.println("main Method");
call();
}
public static void call(){
System.out.println("say hello ...");
}
}
DefineTransformer 類
package org.gk0d;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
// System.out.println("premain load class"+className); //列印載入的類
if ("org/example/Test".equals(className)) {
try {
// 從ClassPool獲得CtClass物件
ClassPool classPool = ClassPool.getDefault();
CtClass ctclass = classPool.get("org.example.Test");
CtMethod call= ctclass.getDeclaredMethod("call");
// 列印後加了一個彈計算器的操作
String MethodBody = "{System.out.println(\"say hello ...\");" +
"java.lang.Runtime.getRuntime().exec(\"calc.exe\");}";
call.setBody(MethodBody);
byte[] bytes = ctclass.toBytecode();
// 返回位元組碼,並且detachCtClass物件
byte[] byteCode = ctclass.toBytecode();
//detach的意思是將記憶體中曾經被javassist載入過的Test物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
ctclass.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 如果返回null則位元組碼不會被修改
return null;
}
}
這種方法存在一定的侷限性——只能在啟動時使用-javaagen
t引數指定。在實際環境中,目標的JVM通常都是已經啟動的狀態,無法預先載入premain
。相比之下,agentmain更加實用。
JDK 1.6 新增了attach (附加方式)方式,可以對執行中的 Java 程序附加 Agent 。
這就是我們說的 agentmain ,使用方式和 permain 十分相似,包括編寫 MANIFEST.MF 和生成代理 Jar 包。但是,它並不需要通過-javaagent
命令列形式引入代理 Jar ,而是在執行時通過 attach 工具啟用指定代理即可
跟premain
函數一樣, 開發者可以編寫一個含有agentmain
函數的 Java 類:
//採用attach機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被載入完成,這個時候需要藉助Instrumentation#retransformClasses(Class<?>... classes)讓對應的類可以重新轉換,從而啟用重新轉換的類執行ClassFileTransformer列表中的回撥
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同樣,agentmain 方法中帶Instrumentation引數的方法也比不帶優先順序更高。開發者必須在 manifest 檔案裡面設定「Agent-Class」來指定包含 agentmain 函數的類。
在Java6 以後實現啟動後載入的新實現是Attach api
。Attach API
很簡單,只有 2 個主要的類,都在 com.sun.tools.attach
包裡面(需要手動匯入):
VirtualMachine
字面意義表示一個Java 虛擬機器器,也就是程式需要監控的目標虛擬機器器,提供了獲取系統資訊(比如獲取記憶體dump、執行緒dump,類資訊統計(比如已載入的類以及範例個數等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(程序id),遠端連線到jvm上 。
代理類注入操作只是它眾多功能中的一個,通過loadAgent方法向jvm註冊一個代理程式agent,在該agent的代理程式中會得到一個Instrumentation範例,該範例可以 在class載入前改變class的位元組碼,也可以在class載入後重新載入。在呼叫Instrumentation範例的方法時,這些方法會使用ClassFileTransformer介面中提供的方法進行處理。
VirtualMachineDescriptor
則是一個描述虛擬機器器的容器類,配合 VirtualMachine 類完成各種功能。
attach 實現注入的原理如下:
通過VirtualMachine類的attach(pid)
方法,便可以attach到一個執行中的java程序上,之後便可以通過loadAgent(agentJarPath)
來將agent的jar包注入到對應的程序,然後對應的程序會呼叫agentmain方法
AgentMain類
package org.gk0d;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import static java.lang.Class.forName;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException,
ClassNotFoundException {
inst.addTransformer(new DefineTransformer(), true);
inst.retransformClasses(Class.forName("org.example.Test"));
}
}
和 premain 的區別在於,我們在 addTransformer 的引數中指定了 true,而且使用了 retransformClasses
重新載入了指定的類。
新建一個自定義的Transformer
package org.gk0d
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class AgentMainTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("org.gk0d.Test".equals(className)) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("org.gk0d.Test");
CtMethod call = ctClass.getDeclaredMethod("call");
// 列印後加了一個彈計算器的操作
String MethodBody = "{java.lang.Runtime.getRuntime().exec(\"calc.exe\");" +
"System.out.println(\"say hello ...\");}";
call.setBody(MethodBody);
byte[] bytes = ctClass.toBytecode();
return bytes;
//detach的意思是將記憶體中曾經被javassist載入過的Test物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
// ctClass.detach();
} catch (Exception e) {
e.printStackTrace();
return classfileBuffer;
}
}else {
return classfileBuffer;
}
}
}
測試AgentMainTest類
將jar通過jvm pid注入進來,使其修改Test類中call方法的位元組碼
package org.gk0d;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class AgentMainTest {
public static void main(String[] args) {
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list(); // 尋找當前系統中所有執行著的JVM程序
for (VirtualMachineDescriptor vmd : list) {
//如果虛擬機器器的名稱為 xxx 則 該虛擬機器器為目標虛擬機器器,獲取該虛擬機器器的 pid
//然後載入 agent.jar 傳送給該虛擬機器器
System.out.println(vmd.displayName()); //vmd.displayName()看到當前系統都有哪些JVM程序在執行
if (vmd.displayName().endsWith("org.gk0d.AgentMainTest")) {
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("D:/vul/agent/agent-1.0-SNAPSHOT.jar");
virtualMachine.detach();
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
}
}
}
}