介绍
Java是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。
Java Agent 就是一种能在不影响正常编译的前提下,修改 Java 字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
agent 主要分为两种:
- jvm 参数启动:实现 premain 方法,在 JVM 启动前加载
- attach 附加启动:实现 agentmain 方法,在 JVM 启动后加载
premain 和 agentmain 函数声明如下,拥有 Instrumentation inst 参数的方法优先级更高:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static void agentmain(String agentArgs, Instrumentation inst) { ... }
public static void agentmain(String agentArgs) { ... }
public static void premain(String agentArgs, Instrumentation inst) { ... }
public static void premain(String agentArgs) { ... }
|
而 agent 内存马就是使用 attach 的方式,实现在程序运行中去注入内存马。
与其他内存马不同的是,它不仅仅可以在反序列化、JNDI 等代码执行的漏洞环境下去注入内存马,还能够在命令注入等情况下实现。
环境搭建
jdk 版本:1.8
agent程序
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 org.agent;
import java.lang.instrument.Instrumentation;
public class SimpleAgent {
public static void premain(String agentArgs, Instrumentation inst) { System.out.println("premain"); }
public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("agentmain"); } }
|
premain 和 agentmain
编译为 jar 包,这里使用插件。在 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
| <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>org.agent.SimpleAgent</Premain-Class> <Agent-Class>org.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>
|
然后使用 maven 直接构建:
测试程序
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 org.agent;
public class BaseMain {
public int print(int i) { System.out.println("i: " + i); return i + 2; }
public void run() { int i = 1; while (true) { i = print(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
public static void main(String[] args) throws InterruptedException { BaseMain main = new BaseMain(); main.run(); Thread.sleep(1000 * 60 * 60); } }
|
jvm 启动前加载
启动程序的时候指定 javaagent 的 jar 路径,然后在主程序启动前就会运行 premain 方法:
1
| -javaagent:D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar
|
IDEA 操作:
启动程序,可以看到在主程序运行前先运行了 premain:
attach 启动后加载
官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。这里需要先导入一下这个 tools 包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.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 AttachMain { public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException { VirtualMachine vm = VirtualMachine.attach("40424"); vm.loadAgent("D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar"); } }
|
查看 PID 号:
VirtualMachine 还有其他方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public abstract class VirtualMachine { public static List<VirtualMachineDescriptor> list() { ... }
public static VirtualMachine attach(String id) { ... }
public abstract void detach() {}
public void loadAgent(String agent) { ... }
}
|
可以借此实现一个遍历,然后去获取到目标 JVM 的 PID 后加载 agent :
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
| package org.agent;
import com.sun.tools.attach.*;
import java.io.IOException; import java.util.List;
public class AttachMain { public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, javassist.NotFoundException { List<VirtualMachineDescriptor> virtualMachineDescriptorList = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : virtualMachineDescriptorList) { if (virtualMachineDescriptor.displayName().contains("org.agent.BaseMain")) { VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id()); virtualMachine.loadAgent("D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); System.out.printf("Loaded agent %s Success !", virtualMachineDescriptor.displayName()); break; } } } }
|
agent 修改 JVM 指定方法
上面的 attach 已经可以实现获取目标 JVM 的 PID 然后去加载 agent 程序,那么 agent 程序该如何去注入内存马?
注入内存马其实就是去修改当前 JVM 中的指定类的指定方法,那么流程就是:
- 获取到指定类
- 获取到指定方法
- 修改方法
修改方法能实现了,那么内存马只是去修改对应环境的请求处理方法,给它加一个命令执行或者启动逻辑就好了。
这个就需要用到 Instrumentation 和 javassist 了。
Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
就是这里的参数:
1
| agentmain(String agentArgs, Instrumentation inst)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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);
Class[] getAllLoadedClasses();
long getObjectSize(Object objectToSize); }
|
通过 Instrumentation 我们可以做到:
getAllLoadedClasses() 获取所有加载的类
addTransformer(ClassFileTransformer transformer, boolean canRetransform) 增加一个转换器
retransformClasses(Class<?>... classes) 重新加载类
agent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); String targetClassName = "org.agent.BaseMain"; for (Class cls : allLoadedClasses) { if (!cls.getName().equals(targetClassName)) { continue; } inst.addTransformer(new AgentTransformer(), true); inst.retransformClasses(cls); } }
|
然后实现这个 ClassFileTransformer transformer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.agent;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { return new byte[0]; } }
|
现在还差的就是 transformer 转换器中去实现修改指定类方法的功能了,这个就需要用到 javassist 去动态修改字节码。
javassist 动态修改字节码
先在 pom.xml 中添加相关依赖:
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.20.0-GA</version> </dependency> </dependencies>
|
CtClass 对象提供了一些方法可以去直接修改目标类。也就可以通过该对象去实现内存马注入。
该对象必须从 ClassPool 对象获取,它就是一个 CtClass 对象容器,然后通过 ClassPool 的 get 方法获取到对应类的 CtClass 对象( 查找对应的类文件,然后封装为 CtClass 对象 )。
如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径,
1 2 3 4 5 6 7 8 9
| ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.get("org.agent.BaseMain");
cp.insertClassPath(new ClassClassPath(<Class>));
CtMethod ctMethod= ctClass.getDeclaredMethod(MethodName)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public final class CtMethod extends CtBehavior { }
public abstract class CtBehavior extends CtMember { public void setBody(String src); public void insertBefore(String src); public void insertAfter(String src); public int insertAt(int lineNum, String src); }
|
修改实现
把着上面两种手段结合起来:
getAllLoadedClasses() 获取所有加载的类
获取到指定的类
addTransformer(ClassFileTransformer transformer, boolean canRetransform) 增加一个转换器,转换器中使用 javassist 去实现方法的修改
- 获取 ctClass 对象容器
- 获取到指定类名的 ctClass 对象
- 获取到指定的方法
- 对方法进行修改
retransformClasses(Class<?>... classes) 重新加载类
实现转换器:
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
| package org.agent;
import javassist.*;
import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
public String className; public String methodName; public String beforeContent;
public AgentTransformer(String className, String methodName, String beforeContent) { this.className = className; this.methodName = methodName; this.beforeContent = beforeContent; }
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { ClassPool classPool = ClassPool.getDefault(); if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); } try { CtClass ctClass = classPool.get(this.className); CtMethod ctMethod = ctClass.getDeclaredMethod(this.methodName); ctMethod.insertBefore(this.beforeContent); return ctClass.toBytecode(); } catch (NotFoundException | CannotCompileException | IOException e) { throw new RuntimeException(e); } } }
|
然后在 agentmain 里面写一下参数:
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 org.agent;
import javassist.convert.Transformer;
import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class SimpleAgent {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); String targetClassName = "org.agent.BaseMain"; String targetMethodName = "print"; String methodBeforeContent = "System.out.println(\"update this method success !\");"; for (Class cls : allLoadedClasses) { if (!cls.getName().equals(targetClassName)) { continue; } inst.addTransformer(new AgentTransformer(targetClassName, targetMethodName, methodBeforeContent), true); inst.retransformClasses(cls); } } }
|
将 agent 重新编译后在 attach 后:
目前可以正常的修改方法,但是实际的环境下,可能是没有 tools 包的所以需要修改 attach 方法为反射实现:
然后编译一下就可以了,需要注意的是 agent 的 jar 路径需要填绝对路径。
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
| package org.agent;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List;
public class AttachReflectionMain { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { if (args.length < 2) { System.out.println("Usage: <class-name> <full-agent-path>"); return; } String className = args[0]; String agentPath = args[1]; Class<?> vmListClass = Class.forName("com.sun.tools.attach.VirtualMachine"); Class<?> vmDescriptorClass = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor"); Method listMethod = vmListClass.getDeclaredMethod("list"); listMethod.setAccessible(true); Object virtualMachineDescriptorList = listMethod.invoke(null); for (int i = 0; i < ((List<?>) virtualMachineDescriptorList).size(); i++) { Object virtualMachineDescriptor = ((List<?>) virtualMachineDescriptorList).get(i); Method displayNameMethod = vmDescriptorClass.getMethod("displayName"); String displayName = (String) displayNameMethod.invoke(virtualMachineDescriptor); if (displayName.contains(className)) { Method attachMethod = vmListClass.getMethod("attach", String.class); Object virtualMachine = attachMethod.invoke(null, ((vmDescriptorClass.getMethod("id")).invoke(virtualMachineDescriptor))); Method loadAgentMethod = vmListClass.getMethod("loadAgent", String.class); loadAgentMethod.invoke(virtualMachine, agentPath); Method detachMethod = vmListClass.getMethod("detach"); detachMethod.invoke(virtualMachine); System.out.printf("Loaded agent %s Success !", displayName); break; } } } }
|
参考链接
源码
AgentStudy.7z