Java 是一种当前广泛使用的编程语言,它可以用于开发各种类型的应用程序,包括桌面应用程序。Java 的跨平台性和易于学习的特点使得它成为开发桌面应用程序的理想选择,然而软件都有出现漏洞的可能性,一旦出现漏洞利用,影响广泛,例如前些年著名的 apache log4j2 漏洞。
在预研一种对用户侧 Java 程序进行监控和防护的方式过程中,遇到的问题是 Java 编译的代码是字节码,在 jvm 中运行,不是可以执行的 CPU 指令,无法以常规 windows hook 方式对程序进行注入和拦截。最终经过探讨,以 asm+ JavaAgent 方式来实现,后续本文将针对 JavaAgent 技术进行说明。
JavaAgent 是 JDK1.5 之后引入的新特性。
JavaAgent 是一种能够在不影响正常编译的情况下,修改字节码的技术。Java 作为一种强类型的语言,不通过编译就不能够进行 jar 包的生成。有了 JavaAgent 技术,就可以在字节码这个层面对类和方法进行修改,可以把 JavaAgent 理解成一种代码注入的方式,或者可以说 Java Agent 就是 JVM 层面的代理程序。
JavaAgent 可以动态修改 Java 字节码,它能够:
①.在加载 Java 字节码之前进行拦截并对字节码进行修改
②.在 Jvm 运行期间修改已经加载的字节码
通过对字节码的修改我们就可以实现对 JAVA 底层源码的重写,在代码生成、代码修改、代码监控、代码分析四个过程中都有重要的应用。可以以 AOP 面向切面编程方式实现性能优化,插件化,热修复等一些安全方面的功能。
1.关于底层 JVMTI
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的了一套代理程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM。JVMTI 的功能非常丰富,包括虚拟机中线程、内存/堆/栈,类/方法/变量,事件/定时器处理等等。
JavaAgent 是依赖 java 底层提供的一个叫 instrument 的 JVMTI Agent,可以将 JavaAgent 理解为 JVM 的一个 “插件”。
JVM TI 可以在 Java 启动随 Java 进程,自动载入共享库。或在 Java 进程运行时,动态加载一个外部基于 JVM TI 编写的 dynamic module 到 Java 进程内,然后触发 JVM 源生线程 Attach Listener 来执行这个 dynamic module 的回调函数。
2.JavaAgent 启动时机
①.在程序启动时通过-javaagent 参数启动代理程序。
②.在程序运行期间通过 Java Tool API 中的 Attach API 动态启动代理程序。
3.JavaAgent 的运行流程
对于 JVM 启动时加载的 Agent 模块代码,Instrumentation 会通过 premain 方法传入代理程序。
premain 方法会在调用程序 main 方法之前被调用,仅限于应用程序的启动时,即 main 函数执行前。premain 方法用于在启动时,在类加载前定义类的 TransFormer(转化器),在类加载的时候更新对应的类的字节码。
关于 JVM 启动后动态加载 Agent 的方法,Instrumentation 会通过 agentmain 方法传入程序。
agentmain 方法在 main 函数开始运行后才被调用,其最大优势是可以在程序运行期间进行字节码的替换。agentmain 方法用于在运行时进行类的字节码的修改,步骤分为注册类的 TransFormer 调用和 retransformClasses 函数进行类的重加载。
在 Java 运行命令中 JavaAgent 是一个参数,用来指定 Agent。
java -javaagent:myagent.jar -cp . examples.Main
启动时修改主要是在 jvm 启动时,执行 native 函数的 Agent_OnLoad 方法,在方法执行时,执行如下步骤:
①.创建 InstrumentationImpl 对象
②.监听 ClassFileLoadHook 事件
③.调用 InstrumentationImpl 的 loadClassAndCallPremain 方法,在这个方法里会去调用 javaagent 里 MANIFEST.MF 里指定的 Premain-Class 类的 premain 方法
运行时修改主要是通过 jvm 的 attach 机制来请求目标 jvm 加载对应的 Agent,执行 native 函数的 Agent_OnAttach 方法,在方法执行时,执行如下步骤:
①.创建 InstrumentationImpl 对象
②.监听 ClassFileLoadHook 事件
③.调用 InstrumentationImpl 的 loadClassAndCallAgentmain 方法在这个方法里会去调用 javaagent 里 MANIFEST.MF 里指定的 Agentmain-Class 类的 agentmain 方法
配合使用比如 ASM,javassist,cglib 等等来改写实现类
在完成了代码开发阶段后,自然还需要对此机制来进行测试验证,以确保它是否能满足我们的功能要求,同时有良好的稳定性和易用性。
在测试过程中,我们重点关注以下几点:
①.jar 文件是否成功进行了静态/动态注入
②.不同 jar 文件之间的依赖关系
③.注入后的 jar 文件是否成功对漏洞进行了热修复
④.不同模块之前的通信是否正常
⑤.jar 文件与运行环境 jre 之间的版本兼容性
⑥.热修复逻辑是否会对同一 jvm 内的其他非目标模块产生影响
⑦.功能可对不同模块进行单独细化控制
在不重新定义类加载器的情况下, 对于已经加载的类重新加载,它是完全独立于应用程序的。通过 Java Agent,可以以一种对应用程序几乎无感知、无侵入的方式添加一些监测能力。对业务透明,可以比较完美的实现热修复的功能。