Java agent(一)
0x00 前言
最近在看字节码插桩和rasp的一些内容,发现其中都绕不开java agent的使用,刚好之前的agent的内存马还没分析过,所以这篇文章先学习一下java agent的知识,后续再继续出rasp的一些内容
0x01 agent基础
我们知道在java中多会有.java文件和.class文件的区分,而.class就是将java文件编译后的字节码文件,然后jvm虚拟机执行的就是class文件,这也是为什么java“一次编译到处运行”
不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。
——>javac xxx.java
使用上面这一条命令就可以将一个java文件编译为class文件,而java agent就是一种可以在不影响正常编译的前提下修改java字节码文件的技术,进而可以达到动态修改已加载或未加载的类,属性以及方法
其实这种技术在平时还是很常见的,就像是热加载补丁或者插件化等
1.1 JAVA Instrumentation Agent
JDK1.5之后,引入了java.lang.instrument包,java基于Instrumentation类约定俗成了两个类方法名:premain和agentmain
Instrumentation类可以与jvmi通讯,来实现类加载的时候获取类信息并改变,而改变类信息就涉及到修改java字节码
修改字节码的工具有两个,一是javassist,操作较人性化,但是效率低;二是asm ,底层复杂,但效率高。下文文中的demo使用的是javassist做演示
官方文档:https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/package-summary.html
- premain

- 使用方法:将agent.jar包在应用运行前通过java -javaagent:<jar 路径> -jar 应用程序.jar来绑定jvm,进而实现class文件的修改
- agentmain

- 使用方法:当我们要修改的类在运行的时候,将agent.jar主动运行,agent获取目前所有运行的jvm,通过特定条件来获取希望修改的jvm-id,然后通过VirtualMachine.attach()方法将agent注入,实现class文件的修改。
1.2 premain-agentDemo
具体代码我放到github上了,代码太长,贴出来影响阅读体验
demo测试
将代码拉下来,在target目录下运行命令:
java -jar -javaagent:./premainDemo-1.0-SNAPSHOT-jar-with-dependencies.jar agent-1.0-SNAPSHOT.jar


发现成功修改class文件中name字段(GEM->wx)
1.3 agentmain-AgentDemo
premain-Agent 只能在 JVM 启动前加载,适用于项目没有运行时对字节码文件进行修改,而如果应用agent技术到内存马的话,都是JVM已经运行了,所以我们看一下agentmain
java通过premain和agentmain给了开发者非常大的权利去控制其他jvm的行为,这种权限最先是希望开发者更好的监控程序的运行,反之也可以被运用于安全防护和破坏,还是非常具有危险性的一个功能。
在写demo之前需要先了解两个类VirtualMachine和VirtualMachineDescriptor
1.31 VirtualMachine
主要用来获取系统信息,内存/现存dump或者类信息统计
- attach:允许通过给attach方法传入一个jvm的pid进而远程连接到jvm上
- loadAgent:注册一个代理agent向jvm,然后运行后会会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
- Detach:解除
Attach
官方文档:https://doc.qzxdp.cn/jdk/20/zh/api/jdk.attach/com/sun/tools/attach/VirtualMachine.html
1.32 VirtualMachineDescriptor
VirtualMachineDescriptor 是用于描述 Java 虚拟机的容器类。它封装了一个标识目标虚拟机的标识符,以及一个对 AttachProvider的引用,在尝试连接到虚拟机时应该使用它。标识符是依赖于实现的,但通常是进程标识符(或 pid)环境,其中每个 Java 虚拟机都在其自己的操作系统进程中运行
VirtualMachineDescriptor(AttachProvider provider, String id)
从给定的组件创建虚拟机描述符。
VirtualMachineDescriptor(AttachProvider provider, String id, String displayName)
从给定的组件创建虚拟机描述符。
官方文档:https://doc.qzxdp.cn/jdk/20/zh/api/jdk.attach/com/sun/tools/attach/VirtualMachineDescriptor.html
demo测试
同理demo代码我贴到了github上,自行参考:
- 先运行Sleep_Hello也就是fake 目标jvm
- 再运行Inject_Agent进行注入
- 发现再运行Sleep_Hello输出"Hello World!"的时候就会输出"调用了agentmain-Agent!",说明agent-main 方法被成功插入

⭐⭐⭐⭐⭐注:
在部署这个源码打包的时候用到了.mf文件,在mf文件中指定agent-class的时候,必须要换行,一定要换行,不然就会报错找不到agent-class!!!!!(不换行他就解析不到mf文件的最后一行)
Manifest-Version: 1.0
Agent-Class: Java_Agent_agentmain
报错如下:

0x02 动态修改字节码
2.1 前置知识:Javassist
javassist是一个用来处理java字节码的类库,即可以在一个已编译好的类中添加新的方法或者修改现有的方法 ,或者手动生成一个新的类对象,使用方式类似于反射
我们上面premain-agentDemo就是使用的javassist
一些javassist的一些主要函数的用法我就不贴了,参考文章:https://www.cnblogs.com/rickiyang/p/11336268.html
rickiyang师傅写的很全了
使用Javassist生成恶意class
之前在cc分析中(如cc3):java反序列化Commons-Collection篇03-CC3链 - Zephyr07 - 博客园
写到过动态加载字节码进行代码执行时,我们的恶意类需要继承AbstractTranslet类并必须重写两个transform方法,否则没法编译成class文件,这里就可以用javassist从字节码层面来生成恶意class,跳过恶意类的编译过程
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import java.io.File;
import java.io.FileOutputStream;
public class createClass {
public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor ctConstructor = ctClass.makeClassInitializer();
ctConstructor.setBody("try{\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception e){\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static void writeShell() throws Exception {
byte[] shell = createClass.getTemplatesImpl("calc");
FileOutputStream fileOutputStream = new FileOutputStream(new File("Sean.class"));
fileOutputStream.write(shell);
}
public static void main (String[]args) throws Exception {
writeShell();
}
}
生成的恶意class文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
public class Evil extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception var1) {
}
}
public Evil() {
}
}
2.2 Instrumentation
一些函数用法参考文章:(个人认为写的很好
JVMI的一部分,java agent通过这个类和目标jvm交互来达到修改数据的效果
在其中有一个叫transformer的class文件转换器可以改变二进制流的数据,可以对未加载或者已加载的类进行拦截,拦截之后就可以使用上述的javassist对拦截下来的类进行动态修改字节码
demo测试
代码在github上:https://github.com/Zephyr1ng/Java-Agent
- idea先运行Hello_Sleep
- 再运行Inject_Agent
- 然后就发现原来在输出hello world,修改之后就把hello world修改为了hacker

2.3 Instrumentation 的局限性
参考了drun1baby师傅的文章,了解到这个实例也会存在一些缺陷
获取Instrumentation接口的实例有两个premain和agentmain,但是这两者回调时机都是类文件字节码读取之后(或者说是类加载之后),但是没办法重新定义一个不存在的类
类的字节码修改就称为类转换,而类转换最终都会回调到Instrumentation#redefineClasses中使用ClassFileTransformer来实现字节码修改,就会产生如下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是 private static/final 修饰的
- 可以修改方法体
0x03 总结:
分析了一圈下来,虽然还没仔细看agent内存马,但也猜到了大致利用流程就是通过在项目运行的时候遍历jvm进程,然后向对应进程中去注入恶意agent类(动态修改字节码为恶意
并且rasp本质上也是将一段程序中注入到一个正常项目中,并且不影响项目的本身的运行,只不过的是这段程序主要是用来保护程序的

浙公网安备 33010602011771号