初探Java安全之JavaAgent

SecIN發表於2022-11-22

About Java Agent

Java Agent的出現

在JDK1.5版本開始,Java增加了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,該功能可以實現JVM再載入某個class檔案對其位元組碼進行修改,也可以對已經載入的位元組碼進行一個重新的載入。而在1.6版本新增了attach(附加方式)方式,可以對執行中的Java程式插入Agent。Java Agent可以去實現位元組碼插樁、動態跟蹤分析等,比如RASP產品和Java Agent記憶體馬。

Java Agent執行模式

有兩種模式:
1、啟動Java程式時新增-javaagent(Instrumentation API實現方式)或-agentpath/-agentlib(JVMTI的實現方式)引數,如java -javaagent:/data/XXX.jar LingXeTest。
2、JDK1.6新增了attach(附加方式)方式,可以對執行中的Java程式附加Agent。

這兩種執行方式的最大區別在於第一種方式只能在程式啟動時指定Agent檔案,而attach方式可以在Java程式執行後根據程式ID動態注入Agent到JVM。

所以類似於想要注入Agent型記憶體馬,一般會用attach的方式。

Java Agent

Javaagent是java命令的一個引數。引數 javaagent 可以用於指定一個jar包

Java Agent和普通的Java類並沒有任何區別,普通的Java程式中規定了main方法為程式入口,而Java Agent則將premain(Agent模式)和agentmain(Attach模式)作為了Agent程式的入口,兩者所接受的引數是完全一致的,如下:

public static void premain(String args, Instrumentation inst) {}
public static void agentmain(String args, Instrumentation inst) {}

而在Attach模式下的premain()方法有兩種寫法,如下:

public static void premain(String agentArgs, Instrumentation inst)
  
public static void premain(String agentArgs)

JVM會去優先載入帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。

Java Agent還限制了我們必須以jar包的形式執行或載入,我們必須將編寫好的Agent程式打包成一個jar檔案。除此之外,Java Agent還強制要求了所有的jar檔案中必須包含/META-INF/MANIFEST.MF檔案,且該檔案中必須定義好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置,如:

Premain-Class: com.anbai.sec.agent.CrackLicenseAgent
Agent-Class: com.anbai.sec.agent.CrackLicenseAgent

如果我們需要修改已經被JVM載入過的類的位元組碼,那麼還需要設定在MANIFEST.MF中新增

Can-Retransform-Classes: true或Can-Redefine-Classes: true。

javaagent引數相關:

-agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>, 例如 -agentlib:hprof
另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
按完整路徑名載入本機代理庫
-javaagent:<jarpath>[=<選項>]
載入 Java 程式語言代理, 請參閱 java.lang.instrument

jarpath 是指向代理程式 JAR 檔案的路徑。options 是代理選項。此開關可以在同一命令列上多次使用,從而建立多個代理程式。多個代理程式可以使用同一 jarpath。代理 JAR 檔案必須符合 JAR 檔案規範。下面的清單屬性是針對代理 JAR 檔案定義的:
Premain-Class
代理類。即包含 premain 方法的類。此屬性是必需的,如果它不存在,JVM 將中止。注:這是類名,而不是檔名或路徑。
Boot-Class-Path
由引導類載入器搜尋的路徑列表。路徑表示目錄或庫(在許多平臺上通常作為 jar 或 zip 庫被引用)。查詢類的特定於平臺的機制出現故障之後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件的語法。如果該路徑以斜槓字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。此屬性是可選的。
Can-Redefine-Classes
布林值(true 或 false,與大小寫無關)。能夠重定義此代理所需的類。值如果不是 true,則被認為是 false。此屬性是可選的,預設值為 false。
代理 JAR 檔案附加到類路徑之後。

而關於java.lang.instrument包位於rt.jar,一共有5個檔案

原始碼簡介

其實這一部分把註釋翻譯過來,有些類和某些方法依舊不理解是什麼意思,也有些看懂了但不知道怎麼用,先鴿著。

ClassDefinition

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;
    }
}

ClassFileTransformer

ClassFileTransformer是一個轉換類檔案的代理介面,我們可以在獲取到Instrumentation物件後透過addTransformer方法新增自定義類檔案轉換器。

使用addTransformer方法可以註冊一個我們自定義的Transformer到Java Agent,當有新的類被JVM載入時JVM會自動回撥用我們自定義的Transformer類的transform方法,傳入該類的transform資訊(類名、類載入器、類位元組碼等),我們可以根據傳入的類資訊決定是否需要修改類位元組碼,修改完位元組碼後我們將新的類位元組碼返回給JVM,JVM會驗證類和相應的修改是否合法,如果符合類載入要求JVM會載入我們修改後的類位元組碼。

package java.lang.instrument;

public interface ClassFileTransformer {

  /**
   * 類檔案轉換方法,重寫transform方法可獲取到待載入的類相關資訊
   *
   * @param loader              定義要轉換的類載入器;如果是引導載入器,則為 null
   * @param className           類名,如:java/lang/Runtime
   * @param classBeingRedefined 如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類載入,則為 null
   * @param protectionDomain    要定義或重定義的類的保護域
   * @param classfileBuffer     類檔案格式的輸入位元組緩衝區(不得修改)
   * @return 位元組碼byte陣列。
   */
  byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                          ProtectionDomain protectionDomain, byte[] classfileBuffer);

}

重寫transform方法需要注意以下事項:

  1. ClassLoader如果是被Bootstrap ClassLoader(引導類載入器)所載入那麼loader引數的值是空。
  2. 修改類位元組碼時需要特別注意插入的程式碼在對應的ClassLoader中可以正確的獲取到,否則會報ClassNotFoundException,比如修改java.io.FileInputStream(該類由Bootstrap ClassLoader載入)時插入了我們檢測程式碼,那麼我們將必須保證FileInputStream能夠獲取到我們的檢測程式碼類。
  3. JVM類名的書寫方式路徑方式:java/lang/String而不是我們常用的類名方式:java.lang.String。
  4. 類位元組必須符合JVM校驗要求,如果無法驗證類位元組碼會導致JVM崩潰或者VerifyError(類驗證錯誤)。
  5. 如果修改的是retransform類(修改已被JVM載入的類),修改後的類位元組碼不得新增方法、修改方法引數、類成員變數。
  6. addTransformer時如果沒有傳入retransform引數(預設是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手動呼叫了retransformClasses方法也一樣無法retransform。
  7. 解除安裝transform時需要使用建立時的Instrumentation例項。

Instrumentation

java.lang.instrument.Instrumentation是監測執行在JVM程式的Java API,利用Instrumentation我們可以實現如下功能:

  1. 動態新增或移除自定義的ClassFileTransformer(addTransformer/removeTransformer),JVM會在類載入時呼叫Agent中註冊的ClassFileTransformer;
  2. 動態修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),將Agent程式新增到BootstrapClassLoader和SystemClassLoaderSearch(對應的是ClassLoader類的getSystemClassLoader方法,預設是sun.misc.Launcher$AppClassLoader)中搜尋;
  3. 動態獲取所有JVM已載入的類(getAllLoadedClasses);
  4. 動態獲取某個類載入器已例項化的所有類(getInitiatedClasses)。
  5. 重定義某個已載入的類的位元組碼(redefineClasses)。
  6. 動態設定JNI字首(setNativeMethodPrefix),可以實現Hook native方法。
  7. 重新載入某個已經被JVM載入過的類位元組碼retransformClasses)。

原始碼如下:

public interface Instrumentation {
  
    //增加一個Class 檔案的轉換器,轉換器用於改變 Class 二進位制流的資料,引數 canRetransform 設定是否允許重新轉換。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在類載入之前,重新定義 Class 檔案,ClassDefinition 表示對一個類新的定義,如果在類載入之後,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之後,後續的類載入都會被Transformer攔截。對於已經載入過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。類載入的位元組碼被修改後,除非再次被retransform,否則不會恢復。
    void addTransformer(ClassFileTransformer transformer);

    //刪除一個類轉換器
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();

    //在類載入之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    boolean isRedefineClassesSupported();

  
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

    boolean isModifiableClass(Class<?> theClass);

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

  
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    //獲取一個物件的大小
    long getObjectSize(Object objectToSize);


   
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

  
    void appendToSystemClassLoaderSearch(JarFile jarfile);

  
    boolean isNativeMethodPrefixSupported();

  
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

Java Agent使用

前面都是理論,我們來簡單寫一個小Demo感受一下如何使用Java Agent技術。

Agent模式

大致分為以下流程(以-javaagent模式為例):

  1. 編寫一個Agent類,其中定義premain方法並呼叫Instrumentation#addTransformer方法新增一個自定義的Transformer
  2. 自定義一個Transformer類,實現Instrumentation介面,在transform方法中寫入自己想要的AOP邏輯
  3. 建立MANIFEST.MF檔案,可以手動寫也可以透過Maven的外掛(pom.xml)
  4. 打包Agent的jar包
  5. 在需要使用JavaAgent的專案新增JVM啟動引數-javaagent並指定我們打包好的jar

這裡需要2個專案,1個為javaagent的jar包,另1個為被javaagent代理的類。最終在被代理類的main方法執行前先執行我們Agent中的premain方法

0x01 編寫javaagent相關程式碼
先建立一個Maven專案,其中建立一個Agent類,裡面需要包含premain方法

package com.zh1z3ven;

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("agentArgs"+agentArgs);
        inst.addTransformer(new DefineTransformer(),true);//呼叫addTransformer新增一個Transformer
    }

}

建立DefineTransformer類,實現ClassFileTransformer介面

package com.zh1z3ven;

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];
    }
}

0x02 建立MANIFEST.MF檔案
手動建立的話需要在resources/META-INF目錄下建立MANIFEST.MF檔案,內容如下:注意多留一行空行

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.zh1z3ven.Agent

透過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>com.zh1z3ven.Agent</Premain-Class>
                            <Agent-Class>com.zh1z3ven.Agent</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>

打包好jar後,檔案會在jar包中

一些可能會用到的引數說明:

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(可選)

0x03 編寫測試類
隨意寫一個

public class a {
    public static void main(String[] args) {
        System.out.println("main Method");
    }
}

0x04 -javaagent模式啟動
JVM啟動引數新增

-javaagent:target/JavaAgent-1.0-SNAPSHOT.jar

執行main方法之前會載入所有的類,包括系統類和自定義類。而在ClassFileTransformer中會去攔截系統類和自己實現的類物件,邏輯則是在ClassFileTransformer實現類的transform方法中定義。
而在這裡transform給我的感覺是類似於一個filter會去攔截/遍歷一些要在JVM中載入的類,而在transform方法中我們可以定義一些邏輯,比如if className== xxx時走入一個邏輯去實現AOP。而其中就可以利用如javassist技術修改位元組碼並作為transform方法的返回值,這樣就在該類在JVM中載入前(-javaagent模式)修改了位元組碼

使用javassist修改位元組碼

這裡在之前a類中新新增一個方法,並在Agent裡我們自定義的Transformerttransform新增一個邏輯,使用javassist去修改我們a類中新新增的方法。

a類中新加一個call方法

package MemoryShell.JavaAgent;

public class a {
    public static void main(String[] args) {
        System.out.println("main Method");
        call();
    }

    public static void call(){
        System.out.println("say hello ...");
    }
}

DefineTransformer

package com.zh1z3ven;

import javassist.*;

import java.io.IOException;
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); //列印載入的類
        if ("MemoryShell/JavaAgent/a".equals(className)){

            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass ctClass = classPool.get("MemoryShell.JavaAgent.a");
                CtMethod call = ctClass.getDeclaredMethod("call");
                // 列印後加了一個彈計算器的操作
                String MethodBody = "{System.out.println(\"say hello ...\");" +
                        "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}";
                call.setBody(MethodBody);
                byte[] bytes = ctClass.toBytecode();
    
                //detach的意思是將記憶體中曾經被javassist載入過的a物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
                ctClass.detach();
                return bytes;

            } catch (NotFoundException e) {
                e.printStackTrace();
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }

}

打成jar包,指定JVM引數後執行a類

-javaagent:target/JavaAgent-1.1-SNAPSHOT.jar

Attach api

在Java SE 6 以後在Instrumentation介面中提供了新的方法agentmain可以在 main 函式開始執行之後再執行。

//採用attach機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被載入完成,這個時候需要藉助Instrumentation#retransformClasses(Class<?>... classes)讓對應的類可以重新轉換,從而啟用重新轉換的類執行ClassFileTransformer列表中的回撥
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

同樣,agentmain 方法中帶Instrumentation引數的方法也比不帶優先順序更高。開發者必須在MANIFEST.MF檔案裡面設定“Agent-Class”來指定包含 agentmain 函式的類。

在Java6 以後實現啟動後載入的新實現是Attach api。Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裡面:

  1. VirtualMachine 字面意義表示一個Java 虛擬機器,也就是程式需要監控的目標虛擬機器,提供了獲取系統資訊(比如獲取記憶體dump、執行緒dump,類資訊統計(比如已載入的類以及例項個數等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現的功能可以說非常之強大 。該類允許我們透過給attach方法傳入一個jvm的pid(程式id),遠端連線到jvm上 。代理類注入操作只是它眾多功能中的一個,透過loadAgent方法向jvm註冊一個代理程式agent,在該agent的代理程式中會得到一個Instrumentation例項,該例項可以 在class載入前改變class的位元組碼,也可以在class載入後重新載入。在呼叫Instrumentation例項的方法時,這些方法會使用ClassFileTransformer介面中提供的方法進行處理。
  2. VirtualMachineDescriptor 則是一個描述虛擬機器的容器類,配合 VirtualMachine 類完成各種功能。

attach實現動態注入的原理如下:

透過VirtualMachine類的attach(pid)方法,便可以attach到一個執行中的java程式上,之後便可以透過loadAgent(agentJarPath)來將agent的jar包注入到對應的程式,然後對應的程式會呼叫agentmain方法。

Attach模式使用

0x01 在JavaAgent專案中新編寫一個AgentMain類

package com.zh1z3ven;

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new AgentMainTransformer(), true);
    }
}

0x02 新建一個自定義的Transformer
transform方法中邏輯依舊是修改a類的call方法位元組碼去彈calc

package com.zh1z3ven;

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 ("MemoryShell.JavaAgent.a".equals(className)) {
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass ctClass = classPool.get("MemoryShell.JavaAgent.a");
                CtMethod call = ctClass.getDeclaredMethod("call");
                // 列印後加了一個彈計算器的操作
                String MethodBody = "{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");" +
                        "System.out.println(\"say hello ...\");}";
                call.setBody(MethodBody);
                byte[] bytes = ctClass.toBytecode();
                return bytes;
                //detach的意思是將記憶體中曾經被javassist載入過的a物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
                //  ctClass.detach();


            } catch (Exception e) {
                e.printStackTrace();
                return classfileBuffer;
            }
        }else {
            return classfileBuffer;
        }


    }


}

0x03 測試AgentMainTest類
將jar透過jvm pid注入進來,使其修改a類中call方法的位元組碼

package MemoryShell.JavaAgent;

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("MemoryShell.JavaAgent.AgentMainTest")) {
                VirtualMachine virtualMachine = null;
                try {
                    virtualMachine = VirtualMachine.attach(vmd.id());
                    virtualMachine.loadAgent("/Users/xxxx/JavaSourceCode/JavaCode/JavaAgent/target/JavaAgent-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();
                }

            }
        }
    }

}

0x04 記得修改MANIFEST.MF或直接改pom.xml

<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>
                <Agent-Class>com.zh1z3ven.AgentMain</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

0x05 打包,先執行測試AgentMainTest類將jar注入進來使其修改a類的位元組碼,之後執行a的main方法,呼叫到call方法時是我們修改過後的位元組碼了,所以會彈calc

Agent模式與Attach模式小結:

  1. 上面Attach這種情況是修改的還沒被JVM載入的類,已載入的Java類是不會再被Agent處理的,這時候我們需要在Attach到目標程式後呼叫instrumentation.redefineClasses,讓JVM重新該Java類,這樣我們就可以使用Agent機制修改該類的位元組碼了。
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException,
     ClassNotFoundException {
     inst.addTransformer(new Transformer(), true);
     inst.retransformClasses(Class.forName("com.example.xxxclass"));
     }
  2. premain和agentmain兩種方式修改位元組碼的時機都是類檔案載入之後,也就是說必須要帶有Class型別的引數,不能透過位元組碼檔案和自定義的類名重新定義一個本來不存在的類。
  3. 類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
  • 新類和老類的父類必須相同;
  • 新類和老類實現的介面數也要相同,並且是相同的介面;
  • 新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
  • 新類和老類新增或刪除的方法必須是private static/final修飾的;
  • 可以修改方法體。
  • java agent 中的所有依賴,在原程式中的 classpath 中都要能找到,否則在注入時原程式會報錯NoClassDefFoundError。
  • agent 程式的 classpath 中必須有 tools.jar(提供 VirtualMachine attach api ),jdk 預設有 tools.jar,jre 預設沒有。並且Linux和Windows之間是存在一個tools.jar適配問題。
  • Reference

    https://www.cnblogs.com/nice0e3/p/14086165.html
    https://www.cnblogs.com/rickiyang/p/11368932.html
    https://su18.org/post/irP0RsYK1/
    javasec.org


    相關文章