JavaAgent

0x00 Intro

IAST(Interactive Application Security Testing)交互式应用程序安全测试

RASP(Runtime application self-protection)运行时应用自我保护。

RASP是一种植入到应用程序内部或其运行时环境的安全技术。RASP可将自身注入到应用程序中,与应用程序融为一体,实时监测、阻断攻击,使程序自身拥有自保护的能力。区别于传统的WAF防护,RASP检测更全面精准、不易被绕过、可预防未知漏洞。

JavaAgent是其背后的技术支持

在Java SE 5及后续版本中,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在JVM上的程序,还能够修改字节码,动态修改已加载或未加载的类以及它们的属性和方法。

0x01 Basic Concept

Java Agent 有两种加载方式:

  • premain, 以指示代理类的方式启动JVM。

  • agentmain, 在JVM启动后的某个时间提供启动代理

premain相当于在main前类加载时进行字节码修改,而agentmain则是main后在类调用前通过重新转换类完成字节码修改。由于加载方式不同,所以premain只能在程序启动时指定Agent文件进行部署,而agentmain需要通过Attach API在程序运行后根据进程ID动态注入agent到jvm中。

关键Instrumentation类(java.lang.instrument):

Java agent通过Instrumentation类和JVM进行交互,从而到达修改字节码的目的。

public interface Instrumentation {
    
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之后,可以使用 retransformClasses 方法重新定义类。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();

    // 在类加载之后,重新定义Class。该方法是1.6之后加入的,事实上,该方法是 update 了一个类。可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();
    
}

启动时加载-premain

  • Agent构造

    • 实现premain方法

    • manifest清单中包含Premain-Class字段

  • 加载Agent

    • 添加-javaagent参数指定Agent(jar包)

    • 在运行main方法前会先调用Agent的premain方法

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

public class MyAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new DefineTransformer(), true);
    }

    static class DefineTransformer implements ClassFileTransformer{

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

jar打包方式:

  • 法一:maven + POM 配置manifestEntries参数

    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.1.0</version>
          <configuration>
            <archive>
              <manifest>
                <addClasspath>true</addClasspath>
              </manifest>
              <manifestEntries>
                <Premain-Class>com.demo.agent.MyAgent</Premain-Class>
                <!--<Agent-Class>com.demo.agent.MyAgent</Agent-Class>-->
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
              </manifestEntries>
            </archive>
          </configuration>
        </plugin>
      </plugins>
    </build>
    1. Premain-Class:包含premain方法的类,需要配置为类的全路径

    2. Agent-Class:包含agentmain方法的类,需要配置为类的全路径

    3. Can-Redefine-Classes:为true时表示能够重新定义Class

    4. Can-Retransform-Classes:为true时表示能够重新转换Class,实现字节码替换

    5. Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀

    执行mvn clean package

  • 法二:MANIFEST.MF配置文件 在资源目录(resources)下,新建目录META-INFMETA-INF目录下,新建文件MANIFEST.MF

    Manifest-Version: 1.0
    Premain-Class: com.demo.agent.MyAgent
    Agent-Class: com.demo.agent.MyAgent
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Can-Set-Native-Method-Prefix: true

    javac .\com\demo\myAgent\MyAgent.java

    jar -cvf MyAgent.jar -C ./ .

运行上加上参数VM options:-javaagent

-javaagent:path\JavaAgent01-1.0-SNAPSHOT.jar

或者通过命令行执行

java -javaagent:path\JavaAgent01-1.0-SNAPSHOT.jar MainTest

注意-javaagent:path要放在MainTest前面

//MainTest.java
public class MainTest {
    public static void main(String[] args) {
        System.out.println("main start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end");
    }
}
  1. 执行main方法之前会加载所有的类,包括系统类和自定义类;

  2. 在ClassFileTransformer中会去拦截系统类和自己实现的类对象;

  3. 要对某些类对象进行改写,在拦截的时候抓住该类使用字节码编译工具即可实现。

agentmain

在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。

agentmain需要通过Attach API在程序运行后根据进程ID动态注入agent到jvm中,利用com.sun.tools.attach.VirtualMachine的attach方法连接目标虚拟机

通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

import java.lang.instrument.Instrumentation;

public class MyAgent {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("AgentMain Start");
    }

}

同上面一样打包为jar

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;
public class AttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //获取当前系统中所有运行中的虚拟机
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为xxx则该虚拟机为目标虚拟机,获取该虚拟机的pid并attach
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.test.Demo")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("path\\JavaAgent01-1.0-SNAPSHOT.jar");
                virtualMachine.detach();
            }
        }
    }
}

main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

若找不到com.sun.tools,在POM中加入tool.jar外部依赖,指定本地tools.jar路径

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8.0</version>
    <scope>system</scope>
    <systemPath>D:\Lang\Java\jdk1.8.0_65\lib\tools.jar</systemPath>
</dependency>

Main测试类

package com.test;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 7; i++) {
            Thread.sleep(3000);
            System.out.println("Hello World");
        }
    }
}

先启动Main测试类,再启动AttachMain来注入jar

由于 tools.jar 并不会在 JVM 启动的时候默认加载,尝试利用URLClassloader来加载tools.jar

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

public class AttachMain {
    public static void main(String[] args) throws Exception {
        File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre", "lib") + java.io.File.separator + "tools.jar");
        System.out.println(toolsPath.toURI().toURL());
        URL url = toolsPath.toURI().toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
        Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");

        Method listMethod = MyVirtualMachine.getDeclaredMethod("list");
        List<Object> list = (List<Object>) listMethod.invoke(MyVirtualMachine);

        for (Object o : list) {
            Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName");
            String name = (String) displayName.invoke(o);
            System.out.println(name);
            if (name.contains("com.test.Demo")) {
                Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id");
                Method attach = MyVirtualMachine.getDeclaredMethod("attach", String.class);
                String id = (String) getId.invoke(o);
                Object vm = attach.invoke(o, id);
                Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent", String.class);
                loadAgent.invoke(vm, "D:\\Code\\Java\\JavaSec\\JavaAgent01\\target\\JavaAgent01-1.0-SNAPSHOT.jar");
                Method detach = MyVirtualMachine.getDeclaredMethod("detach");
                detach.invoke(vm);
            }
        }
    }
}

Last updated