在JDK1.5版本开始,Java增加了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,该功能可以实现JVM再加载某个class文件对其字节码进行修改,也可以对已经加载的字节码进行一个重新的加载。
利用该机制能够实现许多技术,如RASP、内存马、IDEA破解。
Java Agent有两种运行模式:
- 启动Java程序时添加-javaagent(Instrumentation API实现方式)或-agentpath/-agentlib(JVMTI的实现方式)参数,如java -javaagent:/data/XXX.jar Test。
- JDK1.6新增了attach(附加方式)方式,可以对运行中的Java进程附加Agent。
第一种方式只能在程序启动时指定Agent文件,而attach方式可以在Java程序运行后根据进程ID动态注入Agent到JVM。
前置知识使用Java Agent会加载一个jar包(我们的程序也必须打包成一个jar包)。如下有一些规范:
- jar文件中必须包含/meta-INF/MANIFEST.MF文件
- MANIFEST.MF文件中必须定义好Premain-Class(Agent模式)或Agent-Class:(Attach模式)
- 如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: true或Can-Redefine-Classes: true。
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) {}
简单来说就是在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。
premain方法其实还可以简写成:(agentmain同样)
public static void premain(String agentArgs)
JVM会去优先加载带 Instrumentation 签名的premain方法,加载成功忽略无签名的,如果第一种没有,则加载第二种方法。
java.lang.instrument这个包提供了Java运行时,动态修改系统中的Class类型的功能。
这里面有2个重要的接口Instrumentation和 ClassFileTransformer
Instrumentationjava.lang.instrument.Instrumentation是监测运行在JVM程序的Java API,用一下javasec里面的一张图
利用该类可以实现如下功能:
ClassFileTransformer
- 动态添加或移除自定义的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)。
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实例。
看了一堆概念不如代码案例来得实在
JVM运行前(Agent模式)创建我们的agent类
package com.study.agent;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("args: "+agentArgs);
inst.addTransformer(new OurTransformer(),true);//add a Transformer
}
}
OurTransformer类
package com.study.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class OurTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("class: "+ className);//print the class to be loaded
return new byte[0];
}
}
这里由于对maven还不太熟,就用命令行实现的,编译出class文件,要一起编译,否则会报错
javac *.java
后将Agent.class打包成jar文件
jar cvf myagent.jar Agent.class
修改meta-INF目录中MANIFEST.MF文件内容为如上要求:
将jar文件移至out目录下,并创建testanget类,增加其agent
package com.study.agent;
import java.io.IOException;
public class testagent {
public static void main(String[] args) throws IOException {
System.out.println("main");
}
}
运行效果如下:
先去执行我们的agent类(会有一些系统类优先于javaagent进行执行),然后打印JVM加载的类,再到main。(这个字符在Shutdown之前被打印)
要实现更多的操作也是在transform方法中实现,因为ClassFileTransformer中会去拦截系统类和自己实现的类对象,如果需要对某个类进行改写,就可以在拦截的时候抓住这个类后使用字节码编译工具去实现。
推荐学习下javassist:Javassist使用全解析后理解下下面的代码
package com.study.agent;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class OurTransformer2 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;
}
}
JVM运行后(Attach模式)
多出的点在于Java进程的获取,其余不再赘述了都差不多(后面发现是我天真了),详情可移步:Java安全之Java Agent
这里记录下动态注入的原理 :
VirtualMachine类的attach(pid)方法可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
注意点:
- 已加载的Java类是不会再被Agent处理的,这时候我们需要在Attach到目标进程后调用instrumentation.redefineClasses,让JVM重新该Java类,这样我们就可以使用Agent机制修改该类的字节码了。
- premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新类和老类的父类必须相同;
- 新类和老类实现的接口数也要相同,并且是相同的接口;
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
- 新类和老类新增或删除的方法必须是private static/final修饰的;
- 可以修改方法体。
看了上面那么多,这部分才是我们感兴趣的,相信大部分下载使用IDEA破解版的(包括我自己也在用 - -)都添加过类似-javaagent类似的代码,我们来看看这里面的原理到底是什么(全程复现大佬)。
假设我们有一个Java类License,每五秒钟就会自动调用checkExpiry方法检测授权是否过期,如果过期就会一直不断的提示重新购买授权(或者直接退出Java程序)((很模拟))。
检测授权时间是否过期示例代码:
package com.study.agent.LicenseCrack;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class License {
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 5秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
破解这种简单的基于系统时间检测授权是否过期的程序我们有非常多的实现方式,例如:修改系统时间、破解License算法,修改程序授权到期时间、修改检测是否到期类方法的业务逻辑等。
修改类方法业务逻辑又有多种方法,如:反编译类文件,修改类方法、使用字节码编辑工具,修改类方法字节码、使用Java Agent + 字节码编辑工具,在程序校验时修改类字节码。
在不重新编译某个类的情况下(甚至有可能是不重启Java应用服务的情况下)动态的改变类方法的执行逻辑是非常困难的,但如果使用Agent的Instrumentation API就可以非常容易的实现了。
该示例中只需要修改checkExpiry的返回值为false就行了或者修改expireDate参数值为一个100年以后的时间。
(可以提前了解下Hook术语:Hook技术),我们agent代码如下:
package com.study.agent.LicenseCrack;
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;
public class Crack {
private static final String HOOK_CLASS = "com.study.agent.LicenseCrack.License";
public static void premain(String args, final Instrumentation inst) {
loadAgent(args, inst);
}
public static void agentmain(String args, final Instrumentation inst) {
loadAgent(args, inst);
}
public static void main(String[] args) {
if (args.length == 0) {
List 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 = Crack.class.getProtectionDomain().getCodeSource().getLocation();
String agentPath = new File(agentURL.toURI()).getAbsolutePath();
// 注入Agent到目标JVM
vm.loadAgent(agentPath);
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
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重新加载License类的字节码
if (className.equals(HOOK_CLASS)) {
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
}
private static ClassFileTransformer createClassFileTransformer() {
return new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 将目录路径替换成Java类名
className = className.replace("/", ".");
// 只处理com.study.agent.LicenseCrack.License类的字节码
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/com/study/agent/LicenseCrack/"), "Crack.class");
// 写入修改后的字节码到class文件
FileOutputStream fos = new FileOutputStream(classFilePath);
fos.write(classfileBuffer);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
};
}
}
Agent模式
将该文件编译为class文件后按照之前要求打包为jar文件,注意vm那里要多加一个-noverify参数,否则会报java.lang.VerifyErro错误,添加代理
-javaagent:outCrack.jar -noverify
运行效果如下:
重加载的类区别:
Attach模式如果我们希望在License运行时不重启该Java程序的情况下运行我们的破解程序就需要以Attach模式运行了。
运行License程序,运行上述代码获得进程号
(勿忘在MANIFEST.MF文件中加入Agent-Class,Can-Retransform-Classes)运行!
emmm,搜!
好像有了曙光,去修改代码重新编译生成jar文件
还是一样的错误(菜鸡放弃了 - -(就先理解到这里吧))
后记想着瞅瞅JetbrainsCrack-release-enc.jar看看都是什么玩意,先看看它的配置文件
emm,看懂了三四行,没白学!
再见!
更多应用有兴趣可以看看利用 Java Agent 动态修补 Log4j2 漏洞



