Java Agent
Java Agent機制
JDK1.5開始,Java新增了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允許JVM在加載某個class文件之前對其字節碼進行修改,同時也支持對已加載的class(類字節碼)進行重新加載(Retransform)。
利用Java Agent這一特性衍生出了APM(Application Performance Management,應用性能管理)、RASP(Runtime application self-protection,運行時應用自我保護)、IAST(Interactive Application Security Testing,交互式應用程序安全測試)等相關產品,它們都無一例外的使用了Instrumentation/JVMTI的API來實現動態修改Java類字節碼并插入監控或檢測代碼。
Java Agent有兩種運行模式:
- 啟動
Java程序時添加-javaagent(Instrumentation API實現方式)或-agentpath/-agentlib(JVMTI的實現方式)參數,如java -javaagent:/data/XXX.jar LingXeTest。 JDK1.6新增了attach(附加方式)方式,可以對運行中的Java進程附加Agent。
這兩種運行方式的最大區別在于第一種方式只能在程序啟動時指定Agent文件,而attach方式可以在Java程序運行后根據進程ID動態注入Agent到JVM。
Java Agent
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) {}
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。
Instrumentation
java.lang.instrument.Instrumentation是監測運行在JVM程序的Java API,利用Instrumentation我們可以實現如下功能:
- 動態添加或移除自定義的
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)。
Instrumentation類方法如下:

ClassFileTransformer
java.lang.instrument.ClassFileTransformer是一個轉換類文件的代理接口,我們可以在獲取到Instrumentation對象后通過addTransformer方法添加自定義類文件轉換器。
示例中我們使用了addTransformer注冊了一個我們自定義的Transformer到Java Agent,當有新的類被JVM加載時JVM會自動回調用我們自定義的Transformer類的transform方法,傳入該類的transform信息(類名、類加載器、類字節碼等),我們可以根據傳入的類信息決定是否需要修改類字節碼,修改完字節碼后我們將新的類字節碼返回給JVM,JVM會驗證類和相應的修改是否合法,如果符合類加載要求JVM會加載我們修改后的類字節碼。
ClassFileTransformer類代碼:
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方法需要注意以下事項:
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實例。
Agent 實現破解License示例
學習Java Agent除了可以做APM、RASP等產品,我們還可以做一些趣味性事情,比如我們可以使用Agent機制實現Java商業軟件破解,我們常用的IntelliJ IDEA就是使用Agent方式動態修改License類校驗邏輯來實現破解的。
假設我們有一個Java類CrackLicenseTest,每五秒鐘就會自動調用checkExpiry方法檢測授權是否過期,如果過期就會一直不斷的提示重新購買授權(或者直接退出Java程序)。
檢測授權時間是否過期示例代碼:
package com.anbai.sec.agent;
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();
}
}
程序運行結果:
[2020-10-29 23:51:44] 您的授權已過期,請重新購買授權!
[2020-10-29 23:51:49] 您的授權已過期,請重新購買授權!
[2020-10-29 23:51:54] 您的授權已過期,請重新購買授權!
[2020-10-29 23:51:59] 您的授權已過期,請重新購買授權!
[2020-10-29 23:52:04] 您的授權已過期,請重新購買授權!
如果我們要破解這種簡單的基于系統時間檢測授權是否過期的程序我們有非常多的實現方式,例如:修改系統時間、破解License算法,修改程序授權到期時間、修改檢測是否到期類方法的業務邏輯等。
修改類方法業務邏輯又有多種方法,如:反編譯類文件,修改類方法、使用字節碼編輯工具,修改類方法字節碼、使用Java Agent + 字節碼編輯工具,在程序校驗時修改類字節碼。
在不重新編譯某個類的情況下(甚至有可能是不重啟Java應用服務的情況下)動態的改變類方法的執行邏輯是非常困難的,但如果使用Agent的Instrumentation API就可以非常容易的實現了。
破解示例中的CrackLicenseTest的授權檢測方法只需要修改checkExpiry的返回值為false就行了或者修改expireDate參數值為一個100年以后的時間。
破解CrackLicenseTest的授權檢測示例代碼:
/*
* 靈蜥Java Agent版 [Web應用安全智能防護系統]
* ----------------------------------------------------------------------
* Copyright ? 安百科技(北京)有限公司
*/
package com.anbai.sec.agent;
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"), "javaweb-sec-source/javasec-agent/src/main/java/com/anbai/sec/agent/"), "CrackLicenseTest.class");
// 寫入修改后的字節碼到class文件
FileOutputStream fos = new FileOutputStream(classFilePath);
fos.write(classfileBuffer);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
};
}
}
然后再添加pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>javaweb-sec-source</artifactId>
<groupId>com.anbai</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>javasec-agent</artifactId>
<packaging>jar</packaging>
<properties>
<asm.version>9.0</asm.version>
<java.version>1.7</java.version>
<package.name>com.anbai.sec.agent</package.name>
<manifest-file.name>MANIFEST.MF</manifest-file.name>
<maven-jar-plugin.version>2.3.2</maven-jar-plugin.version>
<maven-shade-plugin.version>3.2.2</maven-shade-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version>
</dependency>
<dependency>
<groupId>org.javaweb</groupId>
<artifactId>javaweb-utils</artifactId>
<version>${javaweb.version}</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<finalName>javasec-agent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven-jar-plugin.version}</version>
<configuration>
<archive>
<manifestFile>src/main/resources/${manifest-file.name}</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>MANIFEST.MF</exclude>
<exclude>META-INF/maven/</exclude>
</excludes>
</filter>
</filters>
<artifactSet>
<includes>
<include>org.javassist:javassist:jar:*</include>
<include>org.javaweb:javaweb-utils:jar:*</include>
</includes>
</artifactSet>
<!-- 修改第三方依賴包名稱 -->
<relocations>
<relocation>
<pattern>com.anbai.sec.agent</pattern>
<shadedPattern>${package.name}</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache</pattern>
<shadedPattern>${package.name}.deps.org.apache</shadedPattern>
</relocation>
<relocation>
<pattern>org.javaweb</pattern>
<shadedPattern>${package.name}.deps.org.javaweb</shadedPattern>
</relocation>
<relocation>
<pattern>javassist</pattern>
<shadedPattern>${package.name}.deps.javassist</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
最后再執行如下命令使用Maven構建Agent Jar包:
cd ~/IdeaProjects/javaweb-sec/javaweb-sec-source/javasec-agent
mvn clean install
Maven構建完成后在javaweb-sec/javaweb-sec-source/javasec-agent/target目錄會自動生成一個javasec-agent.jar文件。

Agent模式
如果以Agent模式運行破解程序,需要我們在啟動CrackLicenseTest的時候添加JVM參數:-javaagent:jar路徑,例如:
cd ~/IdeaProjects/javaweb-sec/javaweb-sec-source/javasec-agent
java -javaagent:target/javasec-agent.jar -classpath target/test-classes/ com.anbai.sec.agent.CrackLicenseTest
程序執行結果:

生成的CrackLicenseTest.class如下:

由上示例可以看到CrackLicenseTest類的checkExpiry方法字節碼已經被我們修改成功了 。
Attach模式
如果我們希望在CrackLicenseTest運行時不重啟該Java程序的情況下運行我們的破解程序就需要以Attach模式運行了。Attach模式需要知道我們運行的Java程序進程ID,通過Java虛擬機的進程注入方式實現可以將我們的Agent程序動態的注入到一個已在運行中的Java程序中。
我們可以使用JDK自帶的jps命令獲取本機運行的所有的Java進程,如:
[robert@192:~]$ jps -l
14608 org.jetbrains.jps.cmdline.Launcher
14931 org.jd.gui.OsxApp
1075
6809 org.jetbrains.idea.maven.server.RemoteMavenServer36
15820 com.anbai.sec.agent.CrackLicenseTest
15823 sun.tools.jps.Jps
通過進程的名字com.anbai.sec.agent.CrackLicenseTest就可以找到我們需要注入的進程ID為15823。如果我們想要直接借助Java程序來獲取所有的JVM進程也是可以的,使用com.sun.tools.attach.VirtualMachine的list方法即可獲取本機所有運行的Java進程,如:
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor desc : list) {
System.out.println("進程ID:" + desc.id() + ",進程名稱:" + desc.displayName());
}
有了進程ID我們就可以使用Attach API注入Agent了,Attach Java進程注入示例代碼如下:
// Java進程ID
String pid = args[0];
// 設置Agent文件的絕對路徑
String agentPath = "/xxx/agent.jar";
// 注入到JVM虛擬機進程
VirtualMachine vm = VirtualMachine.attach(pid);
// 注入Agent到目標JVM
vm.loadAgent(agentPath);
vm.detach();
使用Attach模式啟動Agent程序時需要使用到JDK目錄下的lib/tools.jar,如果沒有配置CLASS_PATH環境變量的話需要在運行Agent程序時添加-classpath $JAVA_HOME/lib/tools.jar參數,否則我們無法使用Attach API,如下:
cd ~/IdeaProjects/javaweb-sec/javaweb-sec-source/javasec-agent
java -classpath $JAVA_HOME/lib/tools.jar:target/javasec-agent.jar com.anbai.sec.agent.CrackLicenseAgent
程序執行結果如下:

當Attach成功后我們可以看到原來的進程輸出結果也已經不在輸出授權過期提示信息了,如下圖:

使用Attach模式需要特別的需要注意和Agent模式的區別,因為Attach是運行在Java程序啟動后,所以我們需要修改的Java類很有可能已經被JVM加載了,而已加載的Java類是不會再被Agent處理的,這時候我們需要在Attach到目標進程后retransformClasses,讓JVM重新該Java類,這樣我們就可以使用Agent機制修改該類的字節碼了。
Java Web安全
推薦文章: