手把手教你寫一個Java Agent,實(shí)現(xiàn)“免費(fèi)激活”

前言

相信很多人都“免費(fèi)激活”過 IDEA吧,在IDEA 的vmoptions配置里,加行配置就行:

手把手教你寫一個Java Agent,實(shí)現(xiàn)“免費(fèi)激活”

或者是這樣“拖到IDEA窗口中”的形式:

手把手教你寫一個Java Agent,實(shí)現(xiàn)“免費(fèi)激活”
手把手教你寫一個Java Agent,實(shí)現(xiàn)“免費(fèi)激活”

再或者用過一些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

手把手教你寫一個Java Agent,實(shí)現(xiàn)“免費(fèi)激活”

上面這個例子好像有點(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ù)制代碼

那么怎么修改這個類方法呢?

有兩種方式:

  1. 提前解壓jar包,反編譯那個Class文件,得到Java文件后修改verifyLicense方法后重新編譯
  2. 在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ì)守法的好公民,不然會被請去喝茶就不太好了……

原文鏈接:
https://juejin.cn/post/6944881900636864526

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容