利用Java Agent進行程式碼植入

洋洋的小黑屋 發表於 2021-10-06
Java

利用Java Agent進行程式碼植入

Java Agent 又叫做 Java 探針,是在 JDK1.5 引入的一種可以動態修改 Java 位元組碼的技術。可以把javaagent理解成一種程式碼注入的方式。但是這種注入比起spring的aop更加的優美。

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 的例項,可以用來類定義的轉換和操作等等。

premain方式

JVM啟動時 會先執行 premain 方法,大部分類載入都會通過該方法,注意:是大部分,不是所有。遺漏的主要是系統類,因為很多系統類先於 agent 執行,而使用者類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,就可以結合第三方的位元組碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。

使用例項:

1)建立應用程式Task.jar

先建立一個Task.jar用於模擬在實際場景中的應用程式,Task.java:

public class Task {

    public static void main (String[] args) {
        System.out.println("task mian run");
    }
}

把Task打成jar包:

image-20211005164059111

此jar包可以單獨執行:java -jar Task.jar

image-20211005164321800

2)建立premain方式的Agent

新建一個Agent01.jar,用於在task之前執行:

import java.lang.instrument.Instrumentation;

public class Agent01 {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("premain run----");
    }
}

此時專案如果打成jar包,缺少入口main檔案,所以需要自己定義一個MANIFEST.MF檔案,用於指明premain的入口在哪裡:

src/main/resources/目錄下建立META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.test.Agent01

注意:最後一行是空行,不能省略。以下是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(可選)

同樣的打成jar包:

image-20211005164554249

回顧下我們之前單獨執行task.jar時候,控制檯前後並沒有列印其他資訊

image-20211005164321800

現在我們來使用premain進行注入: java -javaagent:Agent01.jar -jar Task.jar

image-20211005164739343

可以看到premain比task先執行,通過啟動時候指定引數javaagent來達到注入的效果

以下是先知社群師傅的流程圖:

img

這種方法存在一定的侷限性——只能在啟動時使用-javaagent引數指定。在實際環境中,目標的JVM通常都是已經啟動的狀態,無法預先載入premain。相比之下,agentmain更加實用。

agentmain方式

同樣使用一個案例來說明使用方式

使用例項:

1)建立應用程式Task.jar

和之前的premain方式一樣,建立一個Task.jar作為應用程式:

import java.util.Scanner;

public class Task {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        scanner.hasNext();
    }
}

把建立的Task.jar執行起來:java -jar Task.jar

image-20211005180308335

2)建立一個agentmain方式的Agent

建立一個agentmain方式的Agent02.jar,Agent02.java:

import java.lang.instrument.Instrumentation;

public class Agent02 {
    public static void agentmain(String agentArgs, Instrumentation inst){
        System.out.println("列印全部載入的類:");
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
            System.out.println(allLoadedClass.getName());
        }
    }
}

同樣生成jar包的話,需要手動定義一個MANIFEST.MF檔案

Manifest-Version: 1.0
Agent-Class: com.test.Agent02

3)利用VirtualMachine注入

使用VirtualMachine類來利用前面建立的Agent進行代理類注入,VirtualMachine類在jdk目錄下的lib/tools.jar包,需要手動匯入

package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App {    public static void main( String[] args )    {        try {            //VirtualMachine 來自tools.jar            // VirtualMachine.attach("9444") 9444為執行緒PID,使用jps檢視            VirtualMachine vm = VirtualMachine.attach("9444");            //指定要使用的Agent路徑            vm.loadAgent("C:\\Users\\xxx\\Desktop\\Agent02.jar");        } catch (AttachNotSupportedException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (AgentLoadException e) {            e.printStackTrace();        } catch (AgentInitializationException e) {            e.printStackTrace();        }    }}

執行這個名為App的類之後,正在執行的Task程式會執行程式碼:

image-20211003204411098

以下是先知社群的圖:

img

Java Agent 程式碼植入

利用agentmain配合Javassist,在方法執行前,修改任意類的方法。在演示之前,先來看幾個知識點。

Instrumentation類

在agentmain的建構函式中,第二個引數就是Instrumentation

public static void agentmain(String agentArgs, Instrumentation inst)

這個類就是用來進行aop操作的類,能夠替換和修改某些類的定義

public interface Instrumentation {    // 增加一個 Class 檔案的轉換器,轉換器用於改變 Class 二進位制流的資料,引數 canRetransform 設定是否允許重新轉換。在類載入之前,重新定義 Class 檔案,ClassDefinition 表示對一個類新的定義,如果在類載入之後,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之後,後續的類載入都會被Transformer攔截。對於已經載入過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。類載入的位元組碼被修改後,除非再次被retransform,否則不會恢復。    void addTransformer(ClassFileTransformer transformer);    // 刪除一個類轉換器    boolean removeTransformer(ClassFileTransformer transformer);    // 在類載入之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;    // 判斷目標類是否能夠修改。    boolean isModifiableClass(Class<?> theClass);    // 獲取目標已經載入的類。    @SuppressWarnings("rawtypes")    Class[] getAllLoadedClasses();    ......}

其中addTransformer()retransformClasses()用來篡改Class的位元組碼。

從原始碼中看到addTransformer方法引數中,第一個引數傳遞的為ClassFileTransformer型別

image-20211006154929961

ClassFileTransformer介面

這是一個介面,它提供了一個transform方法:

public interface ClassFileTransformer {    default byte[]    transform(  ClassLoader         loader,                String              className,                Class<?>            classBeingRedefined,                ProtectionDomain    protectionDomain,                byte[]              classfileBuffer) {        ....    }}

接下來就用一個示例來演示利用agentmain配合Javassist進行程式碼植入的操作

示例:

1)新建一個hello.jar模擬啟動的應用程式

//HelloWorld.javapublic class HelloWorld {    public static void main(String[] args) {        System.out.println("start...");        hello h1 = new hello();        h1.hello();        // 產生中斷,等待注入        Scanner sc = new Scanner(System.in);        sc.nextInt();        hello h2 = new hello();        h2.hello();        System.out.println("ends...");    }}//hello.javapublic class hello {    public void hello(){        System.out.println("hello world");    }}

2)建立javaAgent.jar

//AgentDemo.javapackage com.test;import java.io.IOException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentDemo {    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {        Class[] classes = inst.getAllLoadedClasses();        // 判斷類是否已經載入        for (Class aClass : classes) {            if (aClass.getName().equals(TransformerDemo.editClassName)) {                // 新增 Transformer                inst.addTransformer(new TransformerDemo(), true);                // 觸發 Transformer                inst.retransformClasses(aClass);            }        }    }}//TransformerDemo.javapackage com.test;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class TransformerDemo implements ClassFileTransformer {    // 只需要修改這裡就能修改別的函式    public static final String editClassName = "com.test.hello";    public static final String editClassName2 = editClassName.replace('.', '/');    public static final String editMethodName = "hello";    @Override    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {        try {            ClassPool cp = ClassPool.getDefault();            if (classBeingRedefined != null) {                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);                cp.insertClassPath(ccp);            }            CtClass ctc = cp.get(editClassName);            CtMethod method = ctc.getDeclaredMethod(editMethodName);            String source = "{System.out.println(\"hello transformer\");}";            method.insertBefore(source);            byte[] bytes = ctc.toBytecode();            ctc.detach();            return bytes;        } catch (Exception e){            e.printStackTrace();        }        return null;    }}

MANIFEST.MF檔案中加入

Manifest-Version: 1.0Agent-Class: com.test.AgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true

3)利用VirtualMachine注入

package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App {    public static void main( String[] args )    {        try {            //VirtualMachine 來自tools.jar            // VirtualMachine.attach("9444") 9444為執行緒PID            VirtualMachine vm = VirtualMachine.attach("9444");            //指定要使用的Agent路徑            vm.loadAgent("C:\\Users\\xxx\\Desktop\\javaAgent.jar");        } catch (AttachNotSupportedException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (AgentLoadException e) {            e.printStackTrace();        } catch (AgentInitializationException e) {            e.printStackTrace();        }    }}

測試:

執行hello.jar

image-20211006160248985

使用VirtualMachine連線VM,進行注入後,第二次呼叫hello方法已經成功增加了一行hello transformer

image-20211006160532716