Java Agent介绍及其使用

一、简介

        Java Agent技术,也被称为Java代理、Java探针,它允许程序员利⽤其构建⼀个独⽴于应⽤程序的代理程序。

        Java Agent 本质上就是一个 jar 包,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。使用Java Agent可以实现在Java程序运行的过程中对其进行修改。

        Java Agent的jar包主要包含两个部分:实现代码与配置文件。配置文件名为MANIFEST.MF,放在META - INF文件夹下,包含下列配置项:

        Manifest-Version: 版本号
        Created-By: 创作者
        Premain-Class :包含 premain 方法的类
        Agent-Class :包含 agentmain 方法的类
        Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false
        Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false 

        Java Agent的入口是premain或agentmain,具体选用哪一个以及其中的内容需要根据应用场景决定。

二、使用场景

        Java Agent 技术有以下主要功能:

  • 在加载java文件前拦截字节码并做修改
  • 在运行期间变更已加载的类的字节码
  •  获取所有已经被加载过的类
  • 获取所有已经被初始化过了的类
  • 获取某个对象的大小

        这些功能使得Java Agent在作为一个独立于Java应用程序的代理程序的同时,可以协助监测、运行甚至替换 JVM 上的程序。Agent的应用十分广泛,可用于实现Java IDE的调试功能、热部署功能、线上诊断⼯具和性能分析⼯具。例如,百度网络攻击防护工具OpenRASP中就使用了Java Agent来对敏感函数进行插桩,以此实现攻击检测。

三、使用方法

        Java Agent分为两种:静态agent与动态agent,这两种方式的启动方法与运行机制是不同的。

1. 静态agent

        这种方式是使用premain作为agent的入口方法,以vm参数的方式载入,在Java程序的 main 方法执行之前执行。要使用agent,需要在premain中实现我们想要的功能。agent技术使用JVMTI提供的接口来实现对应的功能,下面是一些官方的Instrumentation接口:

void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
//注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。布尔值参数canRetransform决定这里被重定义的类是否能够通过retransformClasses方法进行回滚。

void addTransformer(ClassFileTransformer transformer)
//相当于addTransformer(transformer, false),也就是通过ClassFileTransformer实例重定义的类不能进行回滚。

boolean removeTransformer(ClassFileTransformer transformer)
//移除(反注册)ClassFileTransformer实例。

void retransformClasses(Class<?>... classes)
//已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理。

void appendToBootstrapClassLoaderSearch(JarFile jarfile)
//将某个jar加入到Bootstrap Classpath里优先其他jar被加载。

void appendToSystemClassLoaderSearch(JarFile jarfile)
//将某个jar加入到Classpath里供AppClassloard去加载。

Class[] getAllLoadedClasses()
//获取所有已经被加载的类。

Class[] getInitiatedClasses(ClassLoader loader)
//获取所有已经被初始化过了的类。

long getObjectSize(Object objectToSize)
//获取某个对象的(字节)大小,注意嵌套对象或者对象中的属性引用需要另外单独计算。

boolean isModifiableClass(Class<?> theClass)
//判断对应类是否被修改过。

boolean isNativeMethodPrefixSupported()
//是否支持设置native方法的前缀。

boolean isRedefineClassesSupported()
//返回当前JVM配置是否支持重定义类(修改类的字节码)的特性。

boolean isRetransformClassesSupported()
//返回当前JVM配置是否支持类重新转换的特性。

void redefineClasses(ClassDefinition... definitions)
//重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class<?>对象和字节码文件对应的字节数组。

void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)
//设置某些native方法的前缀,主要在找native方法的时候做规则匹配。

        使用静态agent的步骤如下:

        1)编写premain函数,该函数应包含下面两个方法的其中之一:

public static void premain(String agentArgs, Instrumentation inst); 
public static void premain(String agentArgs);

        如果两个方法都被实现了,那么带Instrumentation参数的会被优先调用。agentArgs是premain函数得到的程序参数,通过命令行传入。

        例如,OpenRASP中的premin函数:

/**
* 启动时加载的agent入口方法
*
* @param agentArg 启动参数
* @param inst     {@link Instrumentation}
*/
public static void premain(String agentArg, Instrumentation inst) {
    init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
}

        2)定义一个MANIFEST.MF文件,其中必须包含Premain-Class选项。例如OpenRASP工具中rasp.jar包内的MANIFEST.MF文件如下:

Manifest-Version: 1.0
Build-Time: 2022-03-11T11:55:29Z
Last-Commit-User-Email: lixinkai@baidu.com
Built-By: root
Premain-Class: com.baidu.openrasp.Agent
Created-By: Apache Maven 3.2.3
Git-Branch: 1.3.7
Git-Commit: f264cc6
Last-Commit-User-Name: lixinkai
Build-Jdk: 1.6.0_45
Project-Version: 1.3.7
Agent-Class: com.baidu.openrasp.Agent
Can-Redefine-Classes: true
Main-Class: com.baidu.openrasp.Agent
Can-Retransform-Classes: true
Archiver-Version: Plexus Archiver

        3)将包含premain的类与MANIFEST.MF文件打包成一个 jar 包

        4)在启动java程序时添加启动参数 -javaagent:[path],其中的path为对应的agent的jar包路径。此时java程序的入口函数被agent中的premain取代,premain中的内容将在主程序运行前先执行。

        例如,在安装了OpenRASP的tomcat服务器中,catalina.sh文件中会多出Java Agent的启动参数:

### BEGIN OPENRASP - DO NOT MODIFY ###
	if [ "$RASP_DISABLE"x != "Y"x ]; then JAVA_OPTS="-javaagent:${CATALINA_HOME}/rasp/rasp.jar ${JAVA_OPTS}"; fi
JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED"
export JDK_JAVA_OPTIONS
### END OPENRASP ###

2. 动态agent

        与静态方式不同,动态agent允许代理的目标程序的JVM先启动,再通过attach机制载入。

        和premain模式不同,不再通过添加启动参数的方式来连接agent和主程序了。attach方式使用了com.sun.tools.attach包下的VirtualMachine工具类。需要注意的是这个包不是jvm标准规范,需要引入依赖。以attach方式启动需要在agent中实现如下方法:

public static void agentmain(String agentArgs, Instrumentation inst); 
public static void agentmain(String agentArgs);

        使用动态agent的方式如下:

        1)编写agentmain函数 ,如上面所述。例如OpenRASP也包含了动态agent的入口函数:

/**
* attach 机制加载 agent
*
* @param agentArg 启动参数
* @param inst     {@link Instrumentation}
*/
public static void agentmain(String agentArg, Instrumentation inst) {
    init(Module.START_MODE_ATTACH, agentArg, inst);
}

        2)定义一个MANIFEST.MF文件,与静态agent不同的是,此时必须包含Agent-Class选项。

        3)将包含agentmain的类和MANIFEST.MF文件打成一个jar包。

        4)在主程序开始执行后,通过attach工具加载Agent。Agent启动后会监听VMInit事件,在 JVM 初始化完成之后创建 InstrumentationImpl 对象,监听 ClassFileLoadHook 事件,然后调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,该方法中会调用Agent里MANIFEST.MF所指定的Agent-Class类的agentmain方法。

        在编写agentmain方法时会用以下方法来实现attach机制:

 
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 列出所有VM实例
   
VirtualMachine.attach(descriptor.id());
// attach目标VM
   
VirtualMachine#loadAgent("代理Jar路径","命令参数");
// 目标VM加载Agent

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值