Java 安全之Java Agent

Java 安全之Java Agent

0x00 前言

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

0x01 Java Agent 機制

在JDK1.5版本開始,Java增長了Instrumentation(Java Agent API)JVMTI(JVM Tool Interface)功能,該功能能夠實現JVM再加載某個class文件對其字節碼進行修改,也能夠對已經加載的字節碼進行一個從新的加載。Java Agent能夠去實現字節碼插樁、動態跟蹤分析等。java

Java Aget運行模式

  1. 啓動Java程序的時候添加-javaagent(Instrumentation API實現方式)-agentpath/-agentlib(JVMTI的實現方式)參數shell

  2. 在1.6版本新增了attach(附加方式)方式,能夠對運行中的Java進程插入Agentapache

方式一中只能在啓動前去指定須要加載的Agent文件,而方式二能夠在Java程序運行後根據進程ID進行動態注入Agent到JVM裏面去。編程

0x02 Java Agent 概念

Java Agent是一個Java裏面命令的參數該參數內容能夠指定一個jar包,該jar包內容有必定的規範windows

  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程序的入口。api

若是須要修改已經被JVM加載過的類的字節碼,那麼還須要設置在MANIFEST.MF中添加Can-Retransform-Classes: trueCan-Redefine-Classes: true數組

先來看看命令參數tomcat

命令參數:

-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師傅文中有提到過一些須要注意點和考慮到的點。這個後面再去作實現。

相關文章
相關標籤/搜索