Java 安全之Java Agent
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师傅文中有提到过一些需要注意点和考虑到的点。这个后面再去做实现。