Java 安全之Java Agent

nice_0e3發表於2020-12-04

Java 安全之Java Agent

0x00 前言

在前面發現很多技術都會去採用Java Agent該技術去做實現,比分說RASP和記憶體馬(其中一種方式)、包括IDEA的這些破解都是基於Java Agent去做實現。下面來領略該技術的微妙所在。

0x01 Java Agent 機制

在JDK1.5版本開始,Java增加了Instrumentation(Java Agent API)JVMTI(JVM Tool Interface)功能,該功能可以實現JVM再載入某個class檔案對其位元組碼進行修改,也可以對已經載入的位元組碼進行一個重新的載入。Java Agent可以去實現位元組碼插樁、動態跟蹤分析等。

Java Aget執行模式

  1. 啟動Java程式的時候新增-javaagent(Instrumentation API實現方式)-agentpath/-agentlib(JVMTI的實現方式)引數

  2. 在1.6版本新增了attach(附加方式)方式,可以對執行中的Java程式插入Agent

方式一中只能在啟動前去指定需要載入的Agent檔案,而方式二可以在Java程式執行後根據程式ID進行動態注入Agent到JVM裡面去。

0x02 Java Agent 概念

Java Agent是一個Java裡面命令的引數該引數內容可以指定一個jar包,該jar包內容有一定的規範

  1. jar包中的MANIFEST.MF 檔案必須指定 Premain-Class 項
  2. Premain-Class 指定的那個類必須實現 premain() 方法

上面說到的這個premain方法會在執行main方法前被呼叫,也就是說在執行main方法前會去載入-javaagent指定的jar包裡面的Premain-Class類中的premain方法。那麼其實Java agent本質上就是一個Java的類,但是普通的Java類是以main方法作為程式入口點,而Java Agent則將premain(Agent模式)和agentmain(Attach模式)作為了Agent程式的入口。

如果需要修改已經被JVM載入過的類的位元組碼,那麼還需要設定在MANIFEST.MF中新增Can-Retransform-Classes: trueCan-Redefine-Classes: true

先來看看命令引數

命令引數:

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

上面說到的 java.lang.instrument 提供允許 Java 程式語言代理監測執行在 JVM 上的程式的服務。監測的機制是對方法的位元組碼的修改,在啟動 JVM 時,通過指示代理類 及其代理選項 啟動一個代理程式。

該代理類必須實現公共的靜態 premain 方法,該方法原理上類似於 main 應用程式入口點,並且premain 方法的前面也會有一定的要求,簽名必須滿足一下兩種格式:

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

JVM會去優先載入帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中實現,可以來審計一下該程式碼

例:

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

引數詳細說明:

-javaagent:jarpath[=options]
	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 檔案附加到類路徑之後。

在JDK裡面有個rt.jar包中存在一個java.lang.instrument的包,這個包提供了Java執行時,動態修改系統中的Class型別的功能。但最關鍵的還是javaagent 。它可以在執行時重新接收外部請求,對class型別進行一個修改。

這裡面有2個重要的介面 InstrumentationClassFileTransformer

Instrumentation介面

先來看看Instrumentation介面中的內容

來看到上圖,這是java.lang.instrument.Instrumentation中的一些方法。借鑑一下javasec裡面的一張圖,該圖片描述了各種方法的一個作用

java.lang.instrument.Instrumentation的作用是用來監測執行在JVM中的Java API,利用該類可以實現如下功能:

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

這裡已經表明各大實現功能所對應的方法了。

ClassFileTransformer介面

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

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

檢視一下該介面

該介面中有隻有一個transform方法,裡面的引數內容對應的資訊分別是:

ClassLoader loader              	定義要轉換的類載入器;如果是引導載入器,則為 null
String   className           		載入的類名,如:java/lang/Runtime
Class<?> classBeingRedefined 		如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類載入,則為 null
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例項。

0x03 Java Agent 技術實現

上面說的都是一些概念性的問題,現在去做一個Java agent的實現

來看一下實現的大致幾個步驟

  1. 定義一個 MANIFEST.MF 檔案,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 建立指定的Premain-Class類,並且裡面包含premain 方法,方法邏輯由使用者自己確定
  3. premain MANIFEST.MF檔案打包成一個jar包
  4. 使用 -javaagent: jar引數包路徑 啟動要代理的方法。

完成以上步驟後,啟動程式的時候會去執行premain 方法,當然這個肯定是優先於main方法執行的。但是不免會有一些系統類優先於javaagent進行執行。但是使用者類這些肯定是會被javaagent給攔截下來的。這麼這時候攔截下來後就可以進行一個重寫類等操作,例如使用ASM、javassist,cglib等等來改寫實現類。在實現裡面需要去些2個專案,一個是javaAgent的類,一個是需要JavaAagent需要去代理的類。在mian方法執行前去執行的一些程式碼。

JVM執行前執行

建立一個Agent類,裡面需要包含premain方法:

package com.nice0e3;

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類:

package com.nice0e3;

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

這裡需要重寫transform方法。也就是在載入的時候需要執行操作都會在該方法中進行實現。

SRC\META-INF\MANIFEST.MF檔案中新增內容:

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

我這裡用的是maven去做一個配置

pom.xml:

  <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.nice0e3.Agent</Premain-Class>
                            <Agent-Class>com.nice0e3.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包後,再建立一個專案,配置加入-javaagent引數,-javaagent:out\Agent1-1.0-SNAPSHOT.jar後面不能有多餘的空格。

編寫一個main方法

package com.test;

import java.io.IOException;
import java.io.InputStream;

public class test {


    public static void main(String[] args) throws IOException {
        System.out.println("main");


    }

}

這裡可以看到列印了JVM載入的所有類。而main這個字元再Shutdown之前被列印了,最後面才去載入Shutdown這個也是比較重要的一個點,但是在這裡不做贅述。

前面說過transform方法,也就是在載入的時候需要執行其他的操作都會在該方法中進行實現。這是因為ClassFileTransformer中會去攔截系統類和自己實現的類物件,如果需要對某個類進行改寫,就可以在攔截的時候抓住這個類使用位元組碼編譯工具去實現。

小案例

這裡來複制一個小案例

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
        // 操作Date類
        if ("java/util/Date".equals(className)) {
            try {
                // 從ClassPool獲得CtClass物件
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("java.util.Date");
                CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
                //這裡對 java.util.Date.convertToAbbr() 方法進行了改寫,在 return之前增加了一個 列印操作
                String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
                        "sb.append(name.charAt(1)).append(name.charAt(2));" +
                        "System.out.println(\"sb.toString()\");" +
                        "return sb;}";
                convertToAbbr.setBody(methodBody);

                // 返回位元組碼,並且detachCtClass物件
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是將記憶體中曾經被javassist載入過的Date物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
                clazz.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        // 如果返回null則位元組碼不會被修改
        return null;
    }
}

這裡是使用javassist去動態建立一個類,並且對java.util.DateconvertToAbbr方法去做一個改寫使用setBody插入新的內容,然後轉換成位元組碼進行返回。

JVM執行後執行

前面是使用在main方法執行之前,執行Instrument。而在JDK1.6以後新增的agentmain方法,可以實現在main方法執行以後進行插入執行。

該方法和前面的permain類似,需要定義一個agentmain方法的類。

public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

這個也是和前面的一樣,有Instrumentation型別引數的執行優先順序也是會比沒有該引數的高。

在Java JDK6以後實現啟動後載入Instrument的是Attach api。存在於com.sun.tools.attach裡面有兩個重要的類。

來檢視一下該包中的內容,這裡有兩個比較重要的類,分別是VirtualMachineVirtualMachineDescriptor

VirtualMachine:

VirtualMachine可以來實現獲取系統資訊,記憶體dump、現成dump、類資訊統計(例如JVM載入的類)。裡面配備有幾個方法LoadAgent,Attach 和 Detach 。下面來看看這幾個方法的作用

Attach :從 JVM 上面解除一個代理等方法,可以實現的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(程式id),遠端連線到jvm上

loadAgent:向jvm註冊一個代理程式agent,在該agent的代理程式中會得到一個Instrumentation例項,該例項可以 在class載入前改變class的位元組碼,也可以在class載入後重新載入。在呼叫Instrumentation例項的方法時,這些方法會使用ClassFileTransformer介面中提供的方法進行處理。

Detach:從 JVM 上面解除一個代理(agent)

手動獲取Java程式程式

Attach模式需要知道我們執行的Java程式程式ID,通過Java虛擬機器的程式注入方式實現可以將我們的Agent程式動態的注入到一個已在執行中的Java程式中。我們也可以使用自帶的Jps -l命令去檢視。

看到第一個16320程式估計就是IDEA的破解外掛,使用的Java agent技術進行一個實現破解。

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

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

程式碼自動獲取Java程式程式

package com.nice0e3;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class test {
    public static void main(String[] args) {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
            System.out.println(virtualMachineDescriptor+"\n"+virtualMachineDescriptor.id());
        }
    }
}

有了程式ID後就可以使用Attach API注入Agent了。

動態注入Agent程式碼實現

編輯pom.xml檔案

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

Agent類:

package com.nice0e3;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class Agent {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new DefineTransformer(), true);

    }
}

DefineTransformer類:

package com.nice0e3;

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

編譯成jar包後,編寫一個main方法來進行測試

main方法類:

package com.test;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;


public class test {


    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

        System.out.println("main running");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vir : list) {
            System.out.println(vir.displayName());//列印JVM載入類名
            if (vir.displayName().endsWith("com.test.test")){
                VirtualMachine attach = VirtualMachine.attach(vir.id());   //attach注入一個jvm id注入進去
                attach.loadAgent("out\\Agent1-1.0-SNAPSHOT.jar");//載入agent
                attach.detach();

            }
        }

    }
}

執行結果:

Tips:

  1. 已載入的Java類是不會再被Agent處理的,這時候我們需要在Attach到目標程式後呼叫instrumentation.redefineClasses ,讓JVM重新該Java類,這樣我們就可以使用Agent機制修改該類的位元組碼了。
  2. premain和agentmain兩種方式修改位元組碼的時機都是類檔案載入之後,也就是說必須要帶有Class型別的引數,不能通過位元組碼檔案和自定義的類名重新定義一個本來不存在的類。
  3. 類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
    1. 新類和老類的父類必須相同;
    2. 新類和老類實現的介面數也要相同,並且是相同的介面;
    3. 新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
    4. 新類和老類新增或刪除的方法必須是private static/final修飾的;
    5. 可以修改方法體。

破解IDEA小案例

下面拿一個Javasec的裡面的案例來做一個測試,複製該程式碼

package com.test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * Creator: yz
 * Date: 2020/10/29
 */
public class CrackLicenseTest {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static boolean checkExpiry(String expireDate) {
        try {
            Date date = DATE_FORMAT.parse(expireDate);

            // 檢測當前系統時間早於License授權截至時間
            if (new Date().before(date)) {
                return false;
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }

        return true;
    }

    public static void main(String[] args) {
        // 設定一個已經過期的License時間
        final String expireDate = "2020-10-01 00:00:00";

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        String time = "[" + DATE_FORMAT.format(new Date()) + "] ";

                        // 檢測license是否已經過期
                        if (checkExpiry(expireDate)) {
                            System.err.println(time + "您的授權已過期,請重新購買授權!");
                        } else {
                            System.out.println(time + "您的授權正常,截止時間為:" + expireDate);
                        }

                        // sleep 1秒
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

}

這裡是模擬了一個IDEA的檢測啟用功能。

執行如下

現在需要的就是將這個檢測的啟用的CrackLicenseTest這個類給HOOK掉。

下面來編寫一下程式碼。

package com.nice0e3;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.List;

/**
 * Creator: yz
 * Date: 2020/1/2
 */
public class CrackLicenseAgent {

    /**
     * 需要被Hook的類
     */
    private static final String HOOK_CLASS = "com.anbai.sec.agent.CrackLicenseTest";

    /**
     * Java Agent模式入口
     *
     * @param args 命令引數
     * @param inst Instrumentation
     */
    public static void premain(String args, final Instrumentation inst) {
        loadAgent(args, inst);
    }

    /**
     * Java Attach模式入口
     *
     * @param args 命令引數
     * @param inst Instrumentation
     */
    public static void agentmain(String args, final Instrumentation inst) {
        loadAgent(args, inst);
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            List<VirtualMachineDescriptor> list = VirtualMachine.list();

            for (VirtualMachineDescriptor desc : list) {
                System.out.println("程式ID:" + desc.id() + ",程式名稱:" + desc.displayName());
            }

            return;
        }

        // Java程式ID
        String pid = args[0];

        try {
            // 注入到JVM虛擬機器程式
            VirtualMachine vm = VirtualMachine.attach(pid);

            // 獲取當前Agent的jar包路徑
            URL agentURL = CrackLicenseAgent.class.getProtectionDomain().getCodeSource().getLocation();
            String agentPath = new File(agentURL.toURI()).getAbsolutePath();

            // 注入Agent到目標JVM
            vm.loadAgent(agentPath);
            vm.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 載入Agent
     *
     * @param arg  命令引數
     * @param inst Instrumentation
     */
    private static void loadAgent(String arg, final Instrumentation inst) {
        // 建立ClassFileTransformer物件
        ClassFileTransformer classFileTransformer = createClassFileTransformer();

        // 新增自定義的Transformer,第二個引數true表示是否允許Agent Retransform,
        // 需配合MANIFEST.MF中的Can-Retransform-Classes: true配置
        inst.addTransformer(classFileTransformer, true);

        // 獲取所有已經被JVM載入的類物件
        Class[] loadedClass = inst.getAllLoadedClasses();

        for (Class clazz : loadedClass) {
            String className = clazz.getName();

            if (inst.isModifiableClass(clazz)) {
                // 使用Agent重新載入HelloWorld類的位元組碼
                if (className.equals(HOOK_CLASS)) {
                    try {
                        inst.retransformClasses(clazz);
                    } catch (UnmodifiableClassException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private static ClassFileTransformer createClassFileTransformer() {
        return new ClassFileTransformer() {

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

                // 將目錄路徑替換成Java類名
                className = className.replace("/", ".");

                // 只處理com.anbai.sec.agent.CrackLicenseTest類的位元組碼
                if (className.equals(HOOK_CLASS)) {
                    try {
                        ClassPool classPool = ClassPool.getDefault();

                        // 使用javassist將類二進位制解析成CtClass物件
                        CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

                        // 使用CtClass物件獲取checkExpiry方法,類似於Java反射機制的clazz.getDeclaredMethod(xxx)
                        CtMethod ctMethod = ctClass.getDeclaredMethod(
                                "checkExpiry", new CtClass[]{classPool.getCtClass("java.lang.String")}
                        );

                        // 在checkExpiry方法執行前插入輸出License到期時間程式碼
                        ctMethod.insertBefore("System.out.println(\"License到期時間:\" + $1);");

                        // 修改checkExpiry方法的返回值,將授權過期改為未過期
                        ctMethod.insertAfter("return false;");

                        // 修改後的類位元組碼
                        classfileBuffer = ctClass.toBytecode();
                        File classFilePath = new File(new File(System.getProperty("user.dir"), "src\\main\\java\\com\\nice0e3\\"), "CrackLicenseTest.class");

                        // 寫入修改後的位元組碼到class檔案
                        FileOutputStream fos = new FileOutputStream(classFilePath);
                        fos.write(classfileBuffer);
                        fos.flush();
                        fos.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

                return classfileBuffer;
            }
        };
    }

}

這個不知道為啥自己做的時候沒有成功,貼一張成功的圖過來。

參考文章

https://www.cnblogs.com/rickiyang/p/11368932.html
https://javasec.org/javase/JavaAgent/JavaAgent.html
https://y4er.com/post/javaagent-tomcat-memshell/

0x04 結尾

在中途中會遇到很多坑,比如tools.jar的jar包在windows下找不到,需要手工去Java jdk的lib目錄下然後將該包手工進行新增進去。學習就是一個排坑的過程。假設用Java agent 需要在反序列化或者是直接打入記憶體馬該怎麼去實現?其實y4er師傅文中有提到過一些需要注意點和考慮到的點。這個後面再去做實現。

相關文章