前言
相信很多人都“免費(fèi)激活”過 IDEA吧,在IDEA 的vmoptions配置里,加行配置就行:
或者是這樣“拖到IDEA窗口中”的形式:
再或者用過一些APM工具,在JVM啟動腳本上增加了
-javaagent:/path/to/apm-agent.jar,就可以自動進(jìn)行追蹤。再或者用過Arthas之類的JVM診斷工具,這些工具都是通過Java Agent的技術(shù)去實(shí)現(xiàn)的。**
比如上面說的“免費(fèi)激活”,其實(shí)就是在運(yùn)行時期修改了驗證license的相關(guān)代碼。JAVA 李 Agent 這么強(qiáng)大的功能,你難道不打算自己親自寫一個試試嗎?
基礎(chǔ)知識
Java Agent 算是JVM的一個插件,以一個Jar包的形式存在。可以做到在運(yùn)行時期,修改你的字節(jié)碼文件,從而達(dá)到增強(qiáng)、修改等效果,通過 JVM 提供的 Instrumentation API來實(shí)現(xiàn)。
第一個 Java Agent
一個Java Agent,由以下幾個組件構(gòu)成:
[圖片上傳失敗...(image-db904-1617341457831)]
- Agent Class - Agent的功能類
- Packaging - 在MANIFEST.MF文件中定義Agent Class的位置和方式
- “裝載點(diǎn)”,比如-javaagent:<jarfile>[=arguments],指定加載的agent.jar文件
廢話不多說,下面正式開始編寫這個Agent
1. 創(chuàng)建Agent Class
首先要創(chuàng)建一個Agent Class,這個Class作為我們Agent插件的入口類。配置好-javaagent后,JVM在啟動時會執(zhí)行我們Agent Class的premain方法
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String args, Instrumentation instrumentation){
ClassLoggerTransformer transformer = new ClassLoggerTransformer();
instrumentation.addTransformer(transformer);
}
}
復(fù)制代碼
在premain方法中,除了args參數(shù),還有一個instrumentation對象。這個是Java Agent的核心對象,通過該對象可以注冊ClassFileTransformer。
**ClassFileTransformer **就是負(fù)責(zé)字節(jié)碼轉(zhuǎn)換的核心接口了,已注冊的ClassFileTransformer可以攔截JVM中所有類的加載,并且可以獲取到已加載類的字節(jié)碼,來看一下這個接口的源碼:
public interface ClassFileTransformer {
byte[]
transform( ClassLoader loader,
String className,//className,全類名(包括路徑,"/"分割)
Class<?> classBeingRedefined,//類定義轉(zhuǎn)換時的Class對象,初始加載時為空
ProtectionDomain protectionDomain,//protection...
byte[] classfileBuffer)//加載的Class字節(jié)碼數(shù)據(jù)
throws IllegalClassFormatException;
}
復(fù)制代碼
2. 定義一個Transformer
了解了ClassFileTransformer接口之后,現(xiàn)在來寫一個ClassLoggerTransformer實(shí)現(xiàn)類。為了簡單,這個實(shí)現(xiàn)類只有一個功能:將已加載的字節(jié)碼轉(zhuǎn)儲到文件中
public class ClassLoggerTransformer implements ClassFileTransformer {
//返回值是替換的字節(jié)碼數(shù)據(jù)
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
Path path = Paths.get("classes/" + className + ".class");
//將字節(jié)碼數(shù)據(jù)classfileBuffer,存儲到classes目錄下,以.class文件作為后綴
Files.write(path, classfileBuffer);
} catch (Throwable ignored) { // ignored, don’t do this at home kids
} finally { return classfileBuffer; }
}
}
復(fù)制代碼
好了,第一個Agent 的功能代碼部分已經(jīng)完成了,下面需要 Agent 解決入口的配置
3. 構(gòu)建 agent.jar
現(xiàn)在我們需要將代碼構(gòu)建成一個Jar,并且Jar內(nèi)的MANIFEST.MF文件中,需要包含Agent Class的配置,最終我們的MANIFEST.MF文件應(yīng)該是這樣:
Manifest-Version: 1.0
Premain-Class: com.github.kongwu.agentsamples.firstagent.Agent//Agent Class的全類名
Can-Redefine-Classes: true //允許重新定義
Can-Retransform-Classes: true //允許運(yùn)行時轉(zhuǎn)換
復(fù)制代碼
通過Maven的構(gòu)建插件,很容易完成MANIFEST.MF文件的配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
...
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
...
</plugin>
復(fù)制代碼
只需要在項目的
src/main/resources/META-INF/路徑下,增加一個MANIFEST.MF的模板,模板文件里按照上面介紹的定義即可,最后mvn clean package就可以構(gòu)建出我們的agent jar了(默認(rèn)目錄在${projectpath}/target/),這個jar包內(nèi)會包含我們上面的MANIFEST.MF模板文件
好了,大功告成,我們第一款A(yù)gent已經(jīng)開發(fā)完了,下面來測試一下:
## 隨便找個項目,或者可執(zhí)行的jar包
## 默認(rèn)的啟動方式,直接java -jar
java -jar first-app.jar
## 啟動時添加我們剛才構(gòu)建的first-agent.jar
java -javaagent:/path/to/first-agent.jar -jar first-app.jar
復(fù)制代碼
增加agent運(yùn)行后,所有運(yùn)行時期加載的Class的字節(jié)碼,就會轉(zhuǎn)儲到我們的classes目錄下了
如果你是在 IDEA 中運(yùn)行,也可以在Run/Debug Configurations面板中,add vm options
上面這個例子好像有點(diǎn)過于簡單,只是“攔截”了字節(jié)碼數(shù)據(jù)進(jìn)行了轉(zhuǎn)儲,并沒有進(jìn)行字節(jié)碼的修改。其實(shí)
ClassFileTransformer.transform的返回值,就是我們要替換的數(shù)據(jù);只需要在transform方法中返回新的字節(jié)碼數(shù)據(jù),就可以做到增強(qiáng)/替換類了(不過這個增強(qiáng)/替換是有一些限制的,比如不能修改方法簽名之類的,本文不做過多介紹)
介紹完了基本的Agent實(shí)現(xiàn),下面來學(xué)習(xí)一個Agent的實(shí)際例子:通過Agent來“免費(fèi)激活”
通過Agent 實(shí)現(xiàn)“免費(fèi)激活”
前言中提到的,IDEA“免費(fèi)激活”的工具也是通過Agent實(shí)現(xiàn)的,其實(shí)基本原理很簡單,就是寫一個Agent,動態(tài)修改驗證license的那些代碼而已。
比如我們使用一款需要授權(quán)許可證的Java 軟件,其內(nèi)部驗證許可證的代碼是下面這段(偽代碼)
public boolean verifyLicense(String encryptedLicense){
//請求服務(wù)器驗證許可……
boolean passed = licenseServer.verifyLicense(encryptedLicense);
if(!passed){
//do sth
}
return passed;
}
復(fù)制代碼
那么我們只需要通過Agent,動態(tài)的來修改這個verifyLicense方法,將驗證結(jié)果修改為直接通過,就可以繞過這個許可驗證機(jī)制了,還不用修改原始Jar包
public boolean verifyLicense(String encryptedLicense){
//修改后,直接返回true
return true;
}
復(fù)制代碼
那么怎么修改這個類方法呢?
有兩種方式:
- 提前解壓jar包,反編譯那個Class文件,得到Java文件后修改verifyLicense方法后重新編譯
- 在ClassFileTransformer實(shí)現(xiàn)中,通過傳入的該類字節(jié)碼數(shù)據(jù),使用一些字節(jié)碼操作工具進(jìn)行修改
本文例子為了簡單,使用第一種方式,提前反編譯、修改,再保存重新編譯的Class文件到Agent 項目里:
只需要創(chuàng)建一個ClassFileTransformer,進(jìn)行這個驗證許可證Class的替換:
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class HackVerifierClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//只替換這個驗證的類 - LicenseVerifier
if(className.equals("com/github/kongwu/agentsamples/firstagent/verifierapp/LicenseVerifier")){
return loadHackClassBuffer(loader);
}
return null;
}
private byte[] loadHackClassBuffer(ClassLoader loader) {
//反編譯 -> 修改 -> 重新編譯的LicenseVerifier.class 文件,換個后綴防止被JVM自動加載
try (InputStream input = loader.getResourceAsStream("LicenseVerifier.classdata")){
int n;
ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];//4k
//-1 : end of file
while (-1 != (n = input.read(buffer))) {
outputBuffer.write(buffer,0,n);
}
return outputBuffer.toByteArray();
}catch (IOException e){
System.err.println("load the hackfile failed!");
return null;
}
}
}
復(fù)制代碼
然后在Agent Class中,注冊這個
HackVerifierClassFileTransformer
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String args, Instrumentation instrumentation){
HackVerifierClassFileTransformer transformer = new HackVerifierClassFileTransformer();
instrumentation.addTransformer(transformer);
}
}
復(fù)制代碼
最后只需要像上面那樣,配置下MANIFEST.MF的生成,然后構(gòu)建Agent Jar包,就完成了我們這個“免費(fèi)激活”的Agent 插件
以后運(yùn)行該 Java 軟件時,只需要增加
-javaagent:/path/to/hack-agent.jar,就實(shí)現(xiàn)了“免費(fèi)激活”
## 默認(rèn)的啟動方式,直接java -jar
java -jar verifier-app.jar
## 啟動時添加我們剛才構(gòu)建的agent.jar
java -javaagent:/path/to/hack-agent.jar -jar verifier-app.jar
復(fù)制代碼
文中例子完整的代碼在github.com/kongwu-/age…,有需要的同學(xué)可以自行下載
常用的字節(jié)碼操作類庫
- ASM
- cglib
- javaassist
- ByteBuddy
以上的幾個字節(jié)碼操作類庫,最推薦的是ByteBuddy,使用方式上最簡單
總結(jié)
本文介紹的這個“免費(fèi)激活”的方式,僅用于學(xué)習(xí)交流,不要用于一些非法的場景,做一個遵紀(jì)守法的好公民,不然會被請去喝茶就不太好了……