JAVA Agent内存马笔记

JAVA Agent内存马笔记

本文字数:3018  阅读时长:7分钟

访客数:加载中... | 阅读量:加载中...

Java 是一种强类型语言,在运行之前必须将其编译成.class 字节码,然后再交给 JVM 处理运行。Java Agent 就是一种能在不影响正常编译的前提下,修改 Java 字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于 Java Agent 技术来实现的。那么 Java Agent 技术具体是怎样实现的呢?对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的 premain-Agent,另一种是 JVM 启动之后加载的 agentmain-Agent。这里我们可以将其理解成一种特殊的 Interceptor(拦截器),如下图

premain-Agent

agentmain-Agent

通过这俩张图我们能清楚的看出俩者的不同在于一个是必须在 JVM 启动前加载,而另一种相对,是在 JVM 加载后再进行加载。

premain 和 agentmain 的简单 Demo

这部分通过代码执行实际展示俩者区别。

先写一个 premain-agent

java
package org.example;

import java.lang.instrument.Instrumentation;

public class Agent_Premain {
    public static void premain(String args, Instrumentation inst){
            System.out.println("调用执行了premain_agent");
    }
}

再写一个常规类

java
package org.example;

public class Hello_agent {
    public static void main(String[] args) 
    {
        System.out.println("Hello, agent!");
    }
}

然后再在项目的 main 文件夹下找到 resource 文件夹 (没有则新建),在 resource 文件夹下新建 META-INF 文件夹,在该文件夹下新建 MANIFEST.MF 文件,具体文件结构如图。

MANIFEST.MF 内的内容如下,注意结尾的换行符不能少,不然会标红

java
Manifest-Version: 1.0
Premain-Class: org.example.Agent_Premain

在 pom.xm 里面找准位置添加下述代码,我的是加在 下一行

java
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>2.6</version> <!-- 确保使用最新版本 -->
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestFile>
                        src/main/resources/META-INF/MANIFEST.MF
                    </manifestFile>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

然后再在该页面右键选择 maven 重新加载 pom.xml

安装完插件后点击你 IDEA 右边的 Maven,然后选择 assembly:assembly 进行打包

打包完成后修改 Hello_agent 的运行配置,在修改配置里面选添加虚拟机选项,然后在框框里面加入代码,注意我们选择的是尾巴为 with-dependencies.jar 的 jar 包

java
-javaagent:你的打包好的包的绝对路径
//-javaagent:C:\Users\agent-demo\target\agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar

配置好配置后选择执行,执行效果如下,可以发现 Agent_Premain 的 premain 执行在 jvm 加载执行 Hello_agent 前。

再继续尝试 agentmain,同样的先写一个 agentmain

java
package org.example;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class Agent_Agentmain {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
        System.out.println("调用执行了agentmain");
    }
}

然后更改 MF 文件内容为

java
Manifest-Version: 1.0
Agent-Class: org.example.Agent_Agentmain

因为 agentmain 是可以在 JVM 加载后进行插入,因此我们这次写一个循环的类

java
package org.example;

import static java.lang.Thread.sleep;

public class Helloo_Agent {
    public static void main(String[] args) throws InterruptedException {
        while (true){
            System.out.println("Helloo!");
            sleep(5000);
        }
    }
}

因为 agentmain 和 premain 不同是在 JVM 加载后插入执行的,因此我们需要写一个注入代码实现插入操作

java
package org.example;

import com.sun.tools.attach.*;

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

public class Inject_Agent {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException {
        // 调用 VirtualMachine.list () 获取正在运行的 JVM 列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){
            System.out.println(vmd.displayName());
            // 遍历每一个正在运行的 JVM,如果 JVM 名称为 Sleep_Hello 则连接该 JVM 并加载特定 Agent
            if(vmd.displayName().equals("org.example.Helloo_Agent")){

                // 连接指定 JVM
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                // 加载 Agent
                virtualMachine.loadAgent("C:\\Users\\agent-demo\\target\\agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar");// 这个包的路径和之前添加虚拟机选项选择的包是一个包
                // 断开 JVM 连接
                virtualMachine.detach();
            }

        }
    }
}

先执行 Helloo_Agent 然后再执行 Inject_Agent 进行注入。你可以发现当你执行 Inject_Agent 后,Helloo_Agent 的运行终端会出现调用了 agentmain,这就是执行成功了。

Instrumentation

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

其是 JAVA 中的接口有以下常用方法 (注释取自该文章)

java
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);
}

其中 addTransformer 的作用是增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 var2 设置是否允许重新转换。

在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。

java
public synchronized void addTransformer(ClassFileTransformer var1, boolean var2) {
    if (var1 == null) {
        throw new NullPointerException("null passed as 'transformer' in addTransformer");
    } else {
        if (var2) {
            if (!this.isRetransformClassesSupported()) {
                throw new UnsupportedOperationException("adding retransformable transformers is not supported in this environment");
            }

            if (this.mRetransfomableTransformerManager == null) {
                this.mRetransfomableTransformerManager = new TransformerManager(true);
            }

            this.mRetransfomableTransformerManager.addTransformer(var1);
            if (this.mRetransfomableTransformerManager.getTransformerCount() == 1) {
                this.setHasRetransformableTransformers(this.mNativeAgent, true);
            }
        } else {
            this.mTransformerManager.addTransformer(var1);
        }

    }
}

而 retransformClasses 的作用是在类加载之后,重新定义 Class。该方法是 1.6 之后加入的,事实上,该方法是 update 了一个类。

java
private native void retransformClasses0(long var1, Class<?>[] var3);

public void retransformClasses(Class<?>... var1) {
    if (!this.isRetransformClassesSupported()) {
        throw new UnsupportedOperationException("retransformClasses is not supported in this environment");
    } else {
        this.retransformClasses0(this.mNativeAgent, var1);
    }
}

这里是调用了 retransformClasses0 方法,不过其是 native 方法要去 gitee 上下载源码。该方法在后面的 agent 内存马中有重要作用,这里就先不赘述。

ClassFileTransformer 也是一个接口,不过该接口里只有一个方法,返回一个 bytes 数组

java
public interface ClassFileTransformer {
    byte[] transform(ClassLoader var1, String var2, Class<?> var3, ProtectionDomain var4, byte[] var5) throws IllegalClassFormatException;
}

看了这 3 个源码你是不是已经有注入的思路了,这不就有点类似我们之前学的三大基础的内存马,往正常程序里面夹杂私货吗。那我们就可以写一个新类 Agent_transform,通过获取 org.example.Sleep_Hello 然后向里面添加我们的恶意 transform,然后通过 retransformClasses 触发加载。

java
package org.example;

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

public class Agent_transform {
    public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
        Class [] classes = inst.getAllLoadedClasses();
        // 获取目标 JVM 加载的全部类
        for(Class cls:classes){
            if(cls.getName().equals("org.example.Helloo_Agent")){
                // 添加一个 transformer 到 Instrumentation,并重新触发目标类加载
                inst.addTransformer(new Htransform(), true);
                inst.retransformClasses(cls);
            }
        }
    }
}

恶意类 Htransform

java
package org.example;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class Htransform implements ClassFileTransformer {

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

            // 获取 CtClass 对象的容器 ClassPool
            ClassPool classPool = ClassPool.getDefault();

            // 添加额外的类搜索路径
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(ccp);
            }

            // 获取目标类
            CtClass ctClass = classPool.get("org.example.Helloo_Agent");
            System.out.println(ctClass);

            // 获取目标方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("hello");

            // 设置方法体
            String body = "{System.out.println(\"Hacker!\");}";
            ctMethod.setBody(body);

            // 返回目标类字节码
            byte[] bytes = ctClass.toBytecode();
            return bytes;

        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

修改 Helloo_Agent,添加 hello 方法,并且将 printf 输出改为调用 hello

java
package org.example;

import static java.lang.Thread.sleep;

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

    public static void main(String[] args) throws InterruptedException {
        while (true){
            hello();
            sleep(5000);
        }
    }
}

不过注意要更改 MF 文件,把允许重新加载打开

java
Manifest-Version: 1.0
Agent-Class: org.example.Agent_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

然后按照之前的方法进行构建 jar 包,然后先后运行 Helloo_Agent 和 Inject_Agent,运行结果如下。可以发现成功实现了注入,更改了原本的 hello 方法。

Instrumentation 的局限性

大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性: premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。 类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses 方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是 private static/final 修饰的
  5. 可以修改方法体

上述来自介绍来自https://www.cnblogs.com/F12-blog/p/18122545

其实我还想复现注入 doFilter 的,但是不会调 spring,导致一直找不到可以注入 attach 的进程,所以就没写了,不过思路和上面的大相径庭,不过把要修改的类改成了 doFilter,想复现的可以看这个作者的博客

结语

这篇文章几乎都是照着别的师傅博客的流程写的,算是一个记录复现的笔记吧。其实我之前还是参考了蛮多博客的,不过忘记存链接了,就只能加一点个人的理解,算一个小笔记吧。建议想更加清晰了解的直接看这个师傅的博客

参考链接

  1. https://www.cnblogs.com/F12-blog/p/18122545