手把手教你写一个 Java Agent,实现 “免费激活”- 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

相信很多人都 “免费激活” 过 IDEA 吧,在 IDEA 的 vmoptions 配置里,加行配置就行: JAVA 李 Agent 这么强大的功能,你难道不

相信很多人都 “免费激活” 过 IDEA 吧,在 IDEA 的 vmoptions 配置里,加行配置就行:

https://p3.toutiaoimg.com/origin/pgc-image/1a489c22a3834ac89063da03cd7b01e9?from=pc

或者是这样 “拖到 IDEA 窗口中” 的形式:

https://p3.toutiaoimg.com/origin/pgc-image/4db79ea4f75048c2a0f3c76b64a80563?from=pc

https://p3.toutiaoimg.com/origin/pgc-image/0235f7cde90f4d3583de7981a8fb82dc?from=pc

再或者用过一些 APM 工具,在 JVM 启动脚本上增加了 -javaagent:/path/to/apm-agent.jar,就可以自动进行追踪。再或者用过 Arthas 之类的 JVM 诊断工具,这些工具都是通过 Java Agent 的技术去实现的。**

比如上面说的 “免费激活”,其实就是在运行时期修改了验证 license 的相关代码。JAVA 李 Agent 这么强大的功能,你难道不打算自己亲自写一个试试吗?

Java Agent 算是 JVM 的一个插件,以一个 Jar 包的形式存在。可以做到在运行时期,修改你的字节码文件,从而达到增强、修改等效果,通过 JVM 提供的 Instrumentation API 来实现。

一个 Java Agent,由以下几个组件构成:

https://p3.toutiaoimg.com/origin/pgc-image/3864a03175894744bf72b3fbfdeef658?from=pc

  • Agent Class - Agent 的功能类
  • Packaging - 在 MANIFEST.MF 文件中定义 Agent Class 的位置和方式
  • “装载点” ,比如 - javaagent:[=arguments],指定加载的 agent.jar 文件

废话不多说,下面正式开始编写这个 Agent

首先要创建一个 Agent Class,这个 Class 作为我们 Agent 插件的入口类。配置好 - javaagent 后,JVM 在启动时会执行我们 Agent Class 的 premain 方法

1
2
3
4
5
6
7
8
9
import java.lang.instrument.Instrumentation;

public class Agent {
  public static void premain(String args, Instrumentation instrumentation){
    ClassLoggerTransformer transformer = new ClassLoggerTransformer();
    instrumentation.addTransformer(transformer);
  }
}
复制代码

在 premain 方法中,除了 args 参数,还有一个 instrumentation 对象。这个是 Java Agent 的核心对象,通过该对象可以注册 ClassFileTransformer。

**ClassFileTransformer ** 就是负责字节码转换的核心接口了,已注册的 ClassFileTransformer 可以拦截 JVM 中所有类的加载,并且可以获取到已加载类的字节码,来看一下这个接口的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,//className,全类名(包括路径,"/"分割)
                Class<?>            classBeingRedefined,//类定义转换时的Class对象,初始加载时为空
                ProtectionDomain    protectionDomain,//protection...
                byte[]              classfileBuffer)//加载的Class字节码数据
        throws IllegalClassFormatException;
}

复制代码

了解了 ClassFileTransformer 接口之后,现在来写一个 ClassLoggerTransformer 实现类。为了简单,这个实现类只有一个功能:将已加载的字节码转储到文件中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ClassLoggerTransformer implements ClassFileTransformer {
    
    //返回值是替换的字节码数据
  @Override
  public byte[] transform(ClassLoader loader,
                          String className,
                          Class<?> classBeingRedefined,
                          ProtectionDomain protectionDomain,
                          byte[] classfileBuffer) throws IllegalClassFormatException {
    try {
      Path path = Paths.get("classes/" + className + ".class");
      //将字节码数据classfileBuffer,存储到classes目录下,以.class文件作为后缀
      Files.write(path, classfileBuffer); 
    } catch (Throwable ignored) { // ignored, don’t do this at home kids
    } finally { return classfileBuffer; }
  }
}
复制代码

好了,第一个 Agent 的功能代码部分已经完成了,下面需要 Agent 解决入口的配置

现在我们需要将代码构建成一个 Jar,并且 Jar 内的 MANIFEST.MF 文件中,需要包含 Agent Class 的配置,最终我们的 MANIFEST.MF 文件应该是这样:

1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: com.github.kongwu.agentsamples.firstagent.Agent//Agent Class的全类名
Can-Redefine-Classes: true //允许重新定义
Can-Retransform-Classes: true //允许运行时转换
复制代码

通过 Maven 的构建插件,很容易完成 MANIFEST.MF 文件的配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<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>
复制代码

只需要在项目的 src/main/resources/META-INF / 路径下,增加一个 MANIFEST.MF 的模板,模板文件里按照上面介绍的定义即可,最后 mvn clean package 就可以构建出我们的 agent jar 了(默认目录在 ${projectpath}/target/),这个 jar 包内会包含我们上面的 MANIFEST.MF 模板文件

好了,大功告成,我们第一款 Agent 已经开发完了,下面来测试一下:

1
2
3
4
5
6
7
8
# 随便找个项目,或者可执行的jar包

# 默认的启动方式,直接java -jar
java -jar first-app.jar

# 启动时添加我们刚才构建的first-agent.jar
java -javaagent:/path/to/first-agent.jar -jar first-app.jar
复制代码

增加 agent 运行后,所有运行时期加载的 Class 的字节码,就会转储到我们的 classes 目录下了

如果你是在 IDEA 中运行,也可以在 Run/Debug Configurations 面板中,add vm options

https://p3.toutiaoimg.com/origin/pgc-image/abe01bb55b424ce4935e3f2a07bad690?from=pc

上面这个例子好像有点过于简单,只是 “拦截” 了字节码数据进行了转储,并没有进行字节码的修改。其实 ClassFileTransformer.transform 的返回值,就是我们要替换的数据;只需要在 transform 方法中返回新的字节码数据,就可以做到增强 / 替换类了(不过这个增强 / 替换是有一些限制的,比如不能修改方法签名之类的,本文不做过多介绍)

介绍完了基本的 Agent 实现,下面来学习一个 Agent 的实际例子:通过 Agent 来 “免费激活”

前言中提到的,IDEA“免费激活” 的工具也是通过 Agent 实现的,其实基本原理很简单,就是写一个 Agent,动态修改验证 license 的那些代码而已。

比如我们使用一款需要授权许可证的 Java 软件,其内部验证许可证的代码是下面这段(伪代码)

1
2
3
4
5
6
7
8
9
public boolean verifyLicense(String encryptedLicense){
	//请求服务器验证许可……
    boolean passed = licenseServer.verifyLicense(encryptedLicense);
    if(!passed){
    	//do sth
    }
    return passed;
}
复制代码

那么我们只需要通过 Agent,动态的来修改这个 verifyLicense 方法,将验证结果修改为直接通过,就可以绕过这个许可验证机制了,还不用修改原始 Jar 包

1
2
3
4
5
public boolean verifyLicense(String encryptedLicense){
    //修改后,直接返回true
	return true;
}
复制代码

那么怎么修改这个类方法呢?

有两种方式:

  1. 提前解压 jar 包,反编译那个 Class 文件,得到 Java 文件后修改 verifyLicense 方法后重新编译
  2. 在 ClassFileTransformer 实现中,通过传入的该类字节码数据,使用一些字节码操作工具进行修改

本文例子为了简单,使用第一种方式,提前反编译、修改,再保存重新编译的 Class 文件到 Agent 项目里:

只需要创建一个 ClassFileTransformer,进行这个验证许可证 Class 的替换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
        }
    }
}
复制代码

然后在 Agent Class 中,注册这个 HackVerifierClassFileTransformer

1
2
3
4
5
6
7
8
9
import java.lang.instrument.Instrumentation;

public class Agent {
  public static void premain(String args, Instrumentation instrumentation){
    HackVerifierClassFileTransformer transformer = new HackVerifierClassFileTransformer();
    instrumentation.addTransformer(transformer);
  }
}
复制代码

最后只需要像上面那样,配置下 MANIFEST.MF 的生成,然后构建 Agent Jar 包,就完成了我们这个 “免费激活” 的 Agent 插件

以后运行该 Java 软件时,只需要增加 -javaagent:/path/to/hack-agent.jar,就实现了 “免费激活”

1
2
3
4
5
6
# 默认的启动方式,直接java -jar
java -jar verifier-app.jar

# 启动时添加我们刚才构建的agent.jar
java -javaagent:/path/to/hack-agent.jar -jar verifier-app.jar
复制代码

文中例子完整的代码在 github.com/kongwu-/age…,有需要的同学可以自行下载

  • ASM
  • cglib
  • javaassist
  • ByteBuddy

以上的几个字节码操作类库,最推荐的是 ByteBuddy,使用方式上最简单

本文介绍的这个 “免费激活” 的方式,仅用于学习交流,不要用于一些非法的场景,做一个遵纪守法的好公民,不然会被请去喝茶就不太好了……

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