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運行模式
-
啟動Java程序的時候添加
-javaagent(Instrumentation API實現方式)或-agentpath/-agentlib(JVMTI的實現方式)參數 -
在1.6版本新增了attach(附加方式)方式,可以對運行中的
Java進程插入Agent
方式一中只能在啟動前去指定需要加載的Agent文件,而方式二可以在Java程序運行后根據進程ID進行動態注入Agent到JVM里面去。
0x02 Java Agent 概念
Java Agent是一個Java里面命令的參數該參數內容可以指定一個jar包,該jar包內容有一定的規范
- jar包中的MANIFEST.MF 文件必須指定 Premain-Class 項
- 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: true或Can-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個重要的接口 Instrumentation和 ClassFileTransformer
Instrumentation接口
先來看看Instrumentation接口中的內容


來看到上圖,這是java.lang.instrument.Instrumentation中的一些方法。借鑒一下javasec里面的一張圖,該圖片描述了各種方法的一個作用
java.lang.instrument.Instrumentation的作用是用來監測運行在JVM中的Java API,利用該類可以實現如下功能:
- 動態添加或移除自定義的
ClassFileTransformer(addTransformer/removeTransformer),JVM會在類加載時調用Agent中注冊的ClassFileTransformer; - 動態修改
classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),將Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(對應的是ClassLoader類的getSystemClassLoader方法,默認是sun.misc.Launcher$AppClassLoader)中搜索; - 動態獲取所有
JVM已加載的類(getAllLoadedClasses); - 動態獲取某個類加載器已實例化的所有類(
getInitiatedClasses)。 - 重定義某個已加載的類的字節碼(
redefineClasses)。 - 動態設置
JNI前綴(setNativeMethodPrefix),可以實現Hook native方法。 - 重新加載某個已經被JVM加載過的類字節碼
retransformClasses)。
這里已經表明各大實現功能所對應的方法了。
ClassFileTransformer接口
java.lang.instrument.ClassFileTransformer是一個轉換類文件的代理接口,我們可以在獲取到Instrumentation對象后通過addTransformer方法添加自定義類文件轉換器。
示例中我們使用了addTransformer注冊了一個我們自定義的Transformer到Java Agent,當有新的類被JVM加載時JVM會自動回調用我們自定義的Transformer類的transform方法,傳入該類的transform信息(類名、類加載器、類字節碼等),我們可以根據傳入的類信息決定是否需要修改類字節碼,修改完字節碼后我們將新的類字節碼返回給JVM,JVM會驗證類和相應的修改是否合法,如果符合類加載要求JVM會加載我們修改后的類字節碼。
查看一下該接口

該接口中有只有一個transform方法,里面的參數內容對應的信息分別是:
ClassLoader loader 定義要轉換的類加載器;如果是引導加載器,則為 null
String className 加載的類名,如:java/lang/Runtime
Class<?> classBeingRedefined 如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類加載,則為 null
ProtectionDomain protectionDomain 要定義或重定義的類的保護域
byte[] classfileBuffer 類文件格式的輸入字節緩沖區(不得修改)
重寫transform方法注意事項:
ClassLoader如果是被Bootstrap ClassLoader(引導類加載器)所加載那么loader參數的值是空。- 修改類字節碼時需要特別注意插入的代碼在對應的
ClassLoader中可以正確的獲取到,否則會報ClassNotFoundException,比如修改java.io.FileInputStream(該類由Bootstrap ClassLoader加載)時插入了我們檢測代碼,那么我們將必須保證FileInputStream能夠獲取到我們的檢測代碼類。 JVM類名的書寫方式路徑方式:java/lang/String而不是我們常用的類名方式:java.lang.String。- 類字節必須符合
JVM校驗要求,如果無法驗證類字節碼會導致JVM崩潰或者VerifyError(類驗證錯誤)。 - 如果修改的是
retransform類(修改已被JVM加載的類),修改后的類字節碼不得新增方法、修改方法參數、類成員變量。 addTransformer時如果沒有傳入retransform參數(默認是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手動調用了retransformClasses方法也一樣無法retransform。- 卸載
transform時需要使用創建時的Instrumentation實例。
0x03 Java Agent 技術實現
上面說的都是一些概念性的問題,現在去做一個Java agent的實現
來看一下實現的大致幾個步驟
- 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
- 創建指定的
Premain-Class類,並且里面包含premain方法,方法邏輯由用戶自己確定 - 把
premain和MANIFEST.MF文件打包成一個jar包 - 使用
-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.Date的convertToAbbr方法去做一個改寫使用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里面有兩個重要的類。
來查看一下該包中的內容,這里有兩個比較重要的類,分別是VirtualMachine和VirtualMachineDescriptor

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:
- 已加載的Java類是不會再被Agent處理的,這時候我們需要在Attach到目標進程后調用
instrumentation.redefineClasses,讓JVM重新該Java類,這樣我們就可以使用Agent機制修改該類的字節碼了。 - premain和agentmain兩種方式修改字節碼的時機都是類文件加載之后,也就是說必須要帶有Class類型的參數,不能通過字節碼文件和自定義的類名重新定義一個本來不存在的類。
- 類的字節碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新類和老類的父類必須相同;
- 新類和老類實現的接口數也要相同,並且是相同的接口;
- 新類和老類訪問符必須一致。 新類和老類字段數和字段名要一致;
- 新類和老類新增或刪除的方法必須是private static/final修飾的;
- 可以修改方法體。
破解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師傅文中有提到過一些需要注意點和考慮到的點。這個后面再去做實現。
