作者:Longofo@知道创宇404实验室
时间:2019年12月10日
原文链接:https://paper.seebug.org/1099/
之前在一个应用中搜索到一个类,但是在反序列化测试的时出错,错误不是class notfound
,是其他0xxx
这样的错误,通过搜索这个错误大概是类没有被加载。最近刚好看到了JavaAgent,初步学习了下,能进行拦截,主要通过Instrument Agent来进行字节码增强,可以进行字节码插桩,bTrace,Arthas 等操作,结合ASM,javassist,cglib框架能实现更强大的功能。Java RASP也是基于JavaAgent实现的。趁热记录下JavaAgent基础概念,以及简单使用JavaAgent实现一个获取目标进程已加载的类的测试。
JVMTI与Java Instrument
Java平台调试器架构(Java Platform Debugger Architecture,JPDA)是一组用于调试Java代码的API(摘自维基百科):
- Java调试器接口(Java Debugger
Interface,JDI)——定义了一个高层次Java接口,开发人员可以利用JDI轻松编写远程调试工具 - Java虚拟机工具接口(Java Virtual Machine Tools
Interface,JVMTI)——定义了一个原生(native)接口,可以对运行在Java虚拟机的应用程序检查状态、控制运行 - Java虚拟机调试接口(JVMDI)——JVMDI在J2SE 5中被JVMTI取代,并在Java SE 6中被移除
- Java调试线协议(JDWP)——定义了调试对象(一个 Java 应用程序)和调试器进程之间的通信协议
JVMTI 提供了一套"代理"程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。Instrument Agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),是专门为java语言编写的插桩服务提供支持的代理。
Instrumentation接口
以下接口是Java SE 8 API文档中[1]提供的(不同版本可能接口有变化):
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方法的时候做规则匹配。
redefineClasses与redefineClasses:
重新定义功能在Java SE 5中进行了介绍,重新转换功能在Java SE 6中进行了介绍,一种猜测是将重新转换作为更通用的功能引入,但是必须保留重新定义以实现向后兼容,并且重新转换操作也更加方便。
Instrument Agent两种加载方式
在官方API文档[1]中提到,有两种获取Instrumentation接口实例的方法 :
- JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
- JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。
premain对应的就是VM启动时的Instrument Agent加载,即agent on load
,agentmain对应的是VM运行时的Instrument Agent加载,即agent on attach
。两种加载形式所加载的Instrument Agent
都关注同一个JVMTI
事件 – ClassFileLoadHook
事件,这个事件是在读取字节码文件之后回调时用,也就是说premain和agentmain方式的回调时机都是类文件字节码读取之后(或者说是类加载之后),之后对字节码进行重定义或重转换,不过修改的字节码也需要满足一些要求,在最后的局限性有说明。