技术社区
安全培训
技术社群
积分商城
先知平台
漏洞库
历史记录
清空历史记录
相关的动态
相关的文章
相关的用户
相关的圈子
相关的话题
注册
登录
浅析JavaAgent技术及其应用
K9weirdo
历史精选
774浏览 · 2025-02-24 08:24
返回文档
1、字节码及增强技术
1.1、什么是字节码
Java诞生之初,曾提出过一个广为人知的口号:“
Write Once, Run Anywhere.
” 为了实现这一目标,Sun公司以及其他虚拟机厂商开发了许多能够在不同平台上工作的JVM虚拟机,可以用于加载并执行一种与平台无关的字节码(.class文件)。
通过这种机制,源代码
无需
针对每种平台翻译成对应的机器码,而是被编译成统一的字节码文件,再交由运行在各个平台上的JVM解释和执行,从而实现跨平台运行的能力。
如今,JVM的用途已超越了Java语言本身,催生了许多基于JVM的编程语言,例如Groovy、Scala、Kotlin等,进一步丰富了JVM生态系统。
字节码之所以得名,是因为它的文件内容由十六进制值构成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中,通常使用
javac
命令将源代码编译为字节码文件。一个
.java
文件从编译到运行的过程可以概括如下:
1
源代码编写
:开发者编写
.java
文件,包含程序的逻辑。
2
编译阶段
:通过
javac
命令编译
.java
文件,生成对应的
.class
字节码文件。
3
运行阶段
:使用
java
命令,JVM加载并解析
.class
文件,将其转换为机器可以理解的指令,然后在目标平台上执行。
整个流程体现了Java跨平台的特点,JVM作为中间层,屏蔽了不同硬件和操作系统之间的差异。
当一个
.java
文件通过
javac
编译后,会生成一个
.class
文件。例如,编写一个简单的
Main
类,经过编译后会生成名为
Main.class
的文件。打开该文件后,可以看到一系列以十六进制形式表示的数据。这些数据是按照字节为单位进行分割的。
根据 JVM 规范,每一个字节码文件都必须由十个部分组成,并且这些部分需要按照固定的顺序排列。这个结构确保了
.class
文件的统一性和可解析性。
1.2、字节码增强技术
字节码增强技术是一种通过修改已有的字节码文件或动态生成全新的字节码文件来实现功能扩展的技术。这种技术允许开发者在不直接修改原始源代码的情况下,对程序的行为进行调整或增强。下面我们将介绍几种常见的字节码增强技术
1.2.1、
Javassist
Javassist
是一个功能强大的类库,用于在
源代码层次
操作和处理 Java 字节码。它允许开发者对已经编译好的类进行动态修改,例如添加新方法、修改现有方法,甚至动态生成类。值得注意的是,使用 Javassist 不需要深入了解字节码结构或虚拟机指令,开发者可以通过类似反射的方式轻松实现对类结构的动态操作。
在 Javassist 中,以下四个核心类至关重要:ClassPool、CtClass、CtMethod、CtField
1
CtClass
它是
对字节码文件在代码中的抽象表示
,包含了类的编译时信息如结构等。它是 Javassist 操作类的核心对象。
通过类的全限定名,可以获取到对应的
CtClass
对象。进而可以修改类的定义,例如添加方法、字段或接口,甚至动态生成一个全新的类。
常用方法
:
○
addMethod(CtMethod method)
:向类中添加一个新方法。
○
addField(CtField field)
:向类中添加一个新字段。
○
writeFile(String directory)
:将修改后的类写入文件。
2
ClassPool
用于存储和检索
CtClass
对象的容器。
ClassPool
可以理解为一个存储
CtClass
信息的哈希表,其中键是类的全限定名,值是对应的
CtClass
对象
ClassPool
是 Javassist 的核心,它负责管理所有的类信息。通过它可以加载、创建或修改类。
常用方法
:
○
ClassPool.getDefault()
:获取默认的类池对象。
○
ClassPool.get("className")
:加载指定名称的类,返回一个
CtClass
对象。
3
CtMethod
一个方法的抽象表示
,可以用来
修改现有方法
或
添加新方法
。开发者可以通过它动态调整方法的行为或定义新方法的具体实现。 常用
CtClass.getDeclaredMethod(MethodName)
可以获取对应的CtMethod对象 该类提供了一些方法以便我们能够直接修改方法体。
在使用 Javassist 进行字节码操作时,尤其是在使用
CtMethod.insertBefore()
,
insertAfter()
, 和
insertAt()
等方法插入代码时,可以利用特殊的标识符来访问方法的上下文信息或者改变方法的行为。这些标识符以
$
开头,它们在 Javassist 的内部编译器中有特殊的含义,并且非常利于动态注入代码。
$0
这代表的是方法所在的对象实例(即 this 关键字)。在静态方法中,$0 是 null。
$1, $2, ..., $n
这些标识符代表方法的第一、第二到第 n 个参数。例如,在一个有两个参数的方法中,$1 和 $2 分别代表第一和第二个参数。
$args
这是一个表示所有方法参数的 Object[] 数组。例如,如果一个方法有三个参数,$args 数组将包含三个元素,每个元素分别对应一个参数。
$r
在 insertBefore() 或 insertAt() 中用来表示方法的返回类型。它用于创建一个指定类型的新变量。例如,如果方法返回 int,$r 就可以用来声明一个新的 int 变量。
$w
当方法参数是基本数据类型时,用 $w 可以将其包装成相应的包装类。例如,如果一个方法参数是 int,使用 $w($1) 会得到一个 Integer 对象。
$_
$_ 代表方法的返回值。可以通过修改 $_ 来改变返回值。
$sig
这是一个 Class[] 数组,其中包含了方法的参数类型。这对于反射操作非常有用。
$type
这是一个 Class 对象,代表方法的返回类型。
$class
这代表方法所在的类的 Class 对象
4
CtField
表示类中的一个字段
,可以用来新增或修改字段信息。 通过
CtField
,可以动态向类中添加新的成员变量。
○
常用操作
:
■
new CtField(classPool.get("java.lang.String"), "name", ctClass);
:在**
ctClass
**对应的类中创建一个新的String类型name字段。
■
ctField.setModifiers(int modifiers)
:设置字段的修饰符,例如
public
、
private
等。
●
使用案例
这里我们使用
Javassist
对class文件进行修改
编译后的
Demo
类的字节码文件如下
定义一个
JavassistTest
JavassistTest
编译运行后,会从JVM的
ClassPool
中获取
Demo
类的字节码内容。使用
Javassist
可以修改字节码文件
运行
JavassistTest
后,
Demo.class
文件被修改,并且执行
hello
方法的输出添加了
start
和
end
1.2.2、
ASM
ASM 是一个字节码操作和分析框架,它提供了对字节码的直接操作能力。但是由于ASM 提供了对字节码细节的深入控制,所以ASM 的使用较为复杂,需要深入理解 Java 字节码的结构和指令集。直接操作字节码也意味着开发者必须编写更多的代码来处理具体的字节码指令。
Javassist 允许开发者使用接近 Java 源代码的表达方式,通过简单的 API 调用实现复杂的字节码操作,减少了代码量和复杂性,大大提高了易用性。但因为Javassist 使用过程中需要将Java源代码抽象转换为具体的字节码指令等操作,相对于 ASM会可能引入更多的性能开销。
使用案例
这里我们使用
ASM
修改一个class文件
首先我们的Main 类中定义了
ClassReader
和
ClassWriter
,
ClassReader
读取字节码文件,使得
ClassWriter
初始化时可以直接复制原始字节码中类的结构,然后交给
CustomClassVisitor
类处理,处理完成后由
ClassWriter
写字节码并将旧的字节码替换掉。
定义
CustomClassVisitor
继承自
ClassVisitor
,重写
visitMethod
方法,该方法会判断字节码读到哪一个方法,当读到
hello()
方法时,调用
CustomMethodVisitor
处理
类
CustomMethodVisitor
中的
visitCode
方法,它会在ASM开始访问某一个方法的Code区时被调用,因此重写
visitCode
方法,在方法开始时插入
System.out.println("start");
的字节码
原始字节码
使用ASM修改后
1.2.3、遗留问题
在一个JVM实例中,如果先实例化一个类,然后对其进行字节码增强并重新加载,会导致什么情况发生呢?模拟这种情况,只需在之前提到的Javassist的
JavassistTest
类中的
main()
方法中的第一行加入
Demo d = new Demo();
,即在增强处理之前就让JVM加载了
Demo
类。这时运行就会发现
cc.toClass()
报错了。
因为在JVM中,动态重新加载一个类在运行时是不被允许的。如果只能在类加载之前对类进行增强,那么字节码增强技术的应用场景将受到限制。 但是,利用
Java Agent
技术就可以绕过限制,实现在一个已经加载了所有类且持续运行的JVM中,仍然可以利用字节码增强技术来替换并重新加载其中类的操作。
2、
Java Agent
2.1、
什么是 Java Agent?
Java Agent是一个jar包,但它不能独立运行,而是需要附加到目标JVM进程中。
Java Agent也被称为Java探针,这个称呼相当形象。一旦JVM开始运行,对外部来说,它就像一个黑盒一样。然而,Java Agent就像一支针一样,可以插入到JVM内部,探索其中的内容,并且可以对其进行修改。像一些调试器、线上排查工具、热部署功能等常见场景都是使用了Java Agent技术
2.2、
Java Agent的实现及使用
一个Java Agent主要包含两个部分,一是实现代码,一是配置文件。
●
实现代码:入口类需要实现
agentmain
和
premain
方法,在两个方法中实现具体的功能操作,如读取线程状态、监控数据和修改类的字节码等。
●
配置文件:文件名为 MANIFEST.MF,放在 META-INF 目录下,主要包括配置项:
Manifest-Version
: 版本号 ;
Premain-Class
: premain 方法所在类;
Agent-Class
: agentmain 方法所在类 ;
Can-Redefine-Classes
: 是否可以实现类的重定义;
Can-Retransform-Classes
: 是否可以实现字节码替换
Java Agent可以在应用程序运行之前或之后加载。在应用程序的main方法运行之前,会首先调用Java Agent jar包中的
premain
方法。而在应用程序运行之后即JVM启动后,加载Java Agent jar包时会执行
agentmain
方法。
2.2.1 premain
Java Agent一种启动方式,是通过应用程序的JVM启动参数
-javaagent:xxx.jar
的形式与JVM一起启动,这种情况下,会调用
premain
方法。
我们先定义一个入口类
AgentTest
,实现
agentmain
和
premain
方法
agentArgs
是传递给Agent的参数字符串,如
-javaagent:xxx.jar agentArgs
;
inst
是一个
Instrumentation
接口实例,允许Agent与 JVM 进行交互,允许开发者在JVM运行时检查和修改应用程序类。
并且在pom.xml中配置好指定参数的值
在AgentTest的根目录下运行
mvn clean package
,将其打包
在 IDEA 中配置应用程序启动时的JVM的运行参数,在
VM options
中添加 -javaagent:/path/to/Agent_test-1.0-SNAPSHOT.jar
运行项目后发现在JVM启动前已经调用了premain方法
2.2.2 agentmain
与**
premain
不同,
agentmain
方法是为了之后可以在JVM运行时动态地加载代理而设计的,它可以在JVM启动后的任何时间通过Attach API加载Agent。这样的特性使得
agentmain
**非常适合于不需要重启JVM的情况下,动态地插入监控、调试或修改运行中的应用程序。例如,动态调试、运行时检测和热补丁应用。下面我们来看一下关键的几个类
2.3、动态修改字节码
前面提到了在**
agentmain
**方法中有个参数
Instrumentation inst
是用于获取
Instrumentation
实例的,该类允许开发者在JVM运行时检查和修改应用程序类。
JVM提供了
instrument
这个类库,用于支持Java语言编写的插桩服务,可以修改已加载的类。
在JDK 1.6之前,
instrument
只在JVM启动时加载类时生效;但在JDK 1.6及以后版本,
instrument
支持在运行时修改类定义。
为了利用
instrument
的类修改功能,我们需要实现
ClassFileTransformer
接口,并创建一个类文件转换器。在这个接口中,
transform()
方法会在加载类文件时被调用,允许我们使用ASM或Javassist等技术来改写或替换传入的字节码。
可以看到我们的对
transform()
功能是项目应用中的
TestAgent
类的
hello
方法的输出添加了
start
和
end
接着我们定义agent入口类
AgentTest
,将
Transformer
添加到
Instrumentation
实例中,并借助
agentmain
在后续执行
将agent文件打包成jar包后,我们需要另一个工具将我们的agent动态加载到正在运行的JVM上
2.4、JVMTI
JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套工具接口,用于操作JVM。通过JVMTI,可以实现对JVM的多种操作,它允许注册各种事件勾子,在JVM事件发生时触发这些勾子,从而对不同的JVM事件做出响应。Java Agent可以被看作是JVMTI的一种实现方式。
当Agent需要动态加载到正在运行的JVM上时,就需要借助Attach API 进行实现
这里我们定义一个
Attach_test
类,利用Attach API获取机器上所有正在运行的JVM列表,当找到指定的JVM的时候就加载我们已经打包好的Agent.jar
我们正在运行的spring项目中的
TestAgent
类的
hello()
方法,及调用的
/api/hello
接口如下
运行Spring项目后,运行我们的
Attach_test
类将
Agent_test.jar
注入到spring项目中,显示注入成功
重新访问
/api/hello
接口,发现打印的内容已经新增了
start
和
end
3、JavaAgent安全上的应用
3.1、RASP
运行时应用自我保护(Runtime Application Self-Protection,简称 RASP)是一种通过 Java Agent 技术实现的安全机制。它能够在应用运行期间动态修改类的字节码,将防护逻辑注入到 Java 的底层 API 和 Web 应用程序内部,使安全功能与应用深度集成。通过实时分析和拦截攻击行为,RASP 为应用程序提供了自我保护的能力,帮助其在运行时主动抵御各种 Web 威胁。
结合上面的例子我们实现一个简易的具有针对命令注入漏洞检测和拦截的RASP,其中利用基于Java Agent实现的Hook机制,RASP可以对Java类方法执行前后插入自定义逻辑。
Windows和Linux操作系统的命令执行方法调用过程如下
RASP一般会选择
java.lang.ProcessImpl
和
java.lang.UNIXProcess的<init>或start
方法,我们这里选择
java.lang.ProcessImpl
的start方法作为Hook点
首先,定义一个自定义注解
@HookAnnotation
。这个注解可以作为标记,表示哪些类或方法需要被RASP Agent处理。
接着定义一个针对命令执行方法的Hook类
CmdExecHook
,当检测到执行命令时参数有特殊字符串就检测并拦截,这里也可以使用常见的各种攻击命令作为list合集进行检测
最后定义一个RASP Agent的主类,负责初始化和注册
ClassFileTransformer
这里有一个坑点,由于
Java Agent
默认情况下无法拦截和修改
引导类加载器(Bootstrap ClassLoader)
加载的类,如
java.lang.ProcessImpl
、
java.io.FileInputStream
等标准的 JDK 类,由引导类加载器加载。Java Agent 需要特别配置才能拦截和修改这些类。
所以要解决这个问题,需要确保以下几点:
●
允许 Transformer 拦截引导类加载器中的类
:
○
在
CmdExecHook
的构造方法中,确保
ClassPool
包含引导类路径。
○
使用
Instrumentation
对象添加 Transformer,并指定可以重新转换已加载的类。
●
在 Agent 启动时指定引导类路径
:
○
您需要在 agent 的
premain
方法中将引导类路径添加到
Instrumentation
中,以便能够修改引导类加载器加载的类。
●
重新转换已经加载的类
:
○
如果
java.lang.ProcessImpl
已经在 agent 启动之前被加载,您需要显式地重新转换该类。
因此,在上面的
CmdExecHook
和
RASPAgent
中,都分别实现了引导类路径的添加、重新转换已加载类等操作,从而可以拦截和修改
引导类加载器
加载的类。
将
RASPAgent
打包成jar包后,在主项目运行时加上
-javaagent:"rasp-agent-1.0.0.jar"
,项目运行后显示
RASP
相关的hook加载成功
此时发送命令注入行为的恶意请求包,RASP拦截成功
上面只是实现了一个较为简单的RASP检测和拦截命令注入行为。针对RASP目前有一些常见的绕过思路,主要分为两类:
1、使用没有被限制的类或者函数来绕过(类似绕过黑名单),因此尽量覆盖所有的
贴一张常见的实现类图(来自其他师傅总结)
2、
●
利用更底层的技术进行绕过,如直接hook java底层操作实现的c代码(Java_java_lang_Processlmpl_create等),但是难度较大
●
使用Java本地接口书写程序(Java Native Interface,JNI)绕过 RASP(如,通过修改编译so和dll文件)
●
线程的堆栈绕过
3.2、JavaAgent内存马
Java内存马类型主要有四种:Filter型、Servlet型、Listener型以及Agent型
JDK1.5以后,JavaAgent能够在不影响正常编译的情况下,修改字节码。因此将恶意代码放到项目中的
某个一定会执行
的方法内。
Spring boot 中内嵌了一个
embed Tomcat
作为容器,目前常规Filter型内存马中主要是通过**重写/添加
Filter
**来实现的。因此我们也可以在
Filter
上利用实现Agent型内存马
我们可以查看Spring启动后的调用链
我们查看被反复调用的
ApplicationFilterChain#doFilter()
方法
跟进org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法
我们可以发现以上两个方法均拥有
ServletRequest
和
ServletResponse
,并且并hook 不会影响正常的业务,因此我们在此处进行恶意代码插入
首先,我们定义入口主类
Agent_Memshell
及
agentmain
方法,因为内存马需要在项目运行后动态注入,因此需要
agentmain
方法的执行特性
接着定义一个实现
ClassFileTransformer
接口的
TransformerTest
类,其中对
org.apache.catalina.core.ApplicationFilterChain#doFilter
方法执行初期,插入恶意代码,如果请求包中含有
Cmd
请求头,就对该值进行命令执行
这里有几个可能会碰到的问题
1、注入Agent内存马后,可能会出现
class is Frozen
的报错 因为,一旦一个CtClass对象通过writeFile()、toClass()或者toByteCode()方法转换为class文件,
javassist会对该CtClass对象进行冻结
,阻止进一步的修改操作。这种设计旨在警示开发者避免对已被JVM加载的class文件进行修改,因为JVM不支持重新加载已加载的类。 所以我们可以加入下面代码进行解冻
2、注入Agent内存马后,可能会出现
javassist no found such xxxxx class
的报错
因为可能是Javassist并没有将JVM中某些类文件加载进去
我们可以将JVM中
Class
对象包装成一个
ClassClassPath
对象。这个对象会告诉 Javassist 从
cls
(
Class
对象) 所属的位置加载类文件
解决上面两个问题后我们可以使用命令
mvn clean package
将上面的内存马文件打包成jar包并且上传到主应用服务器上
打包成功后我们需要借助JVM TI的Attach API 将Agent内存马动态加载到正在运行的项目的JVM上
这里有一个问题,使用Attach API的话需要调用
com.sun.tools.attach.VirtualMachine
类,该类属于 JDK 的
tools.jar
,这个库并不总是在 Java 运行时环境(JRE)中可用,它通常存在于 Java 开发工具包(JDK)中。所以,这个
tools.jar
在JVM启动的时候并不会默认加载。
因此我们可以使用
URLClassLoader
加载项目机器
tools.jar
并通过反射调用其方法,可以动态地解决这个依赖问题,允许代码在没有直接包含
tools.jar
的运行时环境中执行。
(注:如果应用部署在仅含有 JRE 的环境中,并且该应用所在的服务器上也并没有装jdk,那么无论是通过反射还是直接引用,
tools.jar
这个包都不会存在于环境中。因此无法利用)
下面我们实现
Inject_Memshell
类,用于将Agent内存马注入项目中
使用命令
mvn clean package
将上面的
Inject_Memshell
文件打包成jar包,并且将主项目也打包成jar包
运行主项目
java -jar DemoApplication-1.0-SNAPSHOT.jar
正常访问请求,此时Cmd请求头并没有生效
在主项目的服务器上运行打包好的
Inject_Memshell
的jar包:
java -jar Inject_Memshell-1.0-SNAPSHOT.jar
,返回 Inject Success!
在项目的终端也显示注入成功
此时发出恶意请求,命令执行成功
目前常见的是Agent类型内存马利用是上传 agent.jar 到服务器用来承载webshell功能。冰蝎服务端会调用Java API将 agent.jar 植入自身进程完成注入。
冰蝎的开发者rebeyond师傅在
《Java内存攻击技术漫谈》
,提出了无文件agent植入技术,整个Agent注入的过程不需要在目标磁盘上落地文件,但是会有一定概率会导致项目崩溃。在
《论如何优雅的注入Java Agent内存马》
中提出了一种新的无文件植入内存马技术,并集成在了冰蝎v4.0中
4、总结
文章浅析了JavaAgent技术在不同场景下的实现与应用。通过分析字节码操作工具(如Javassist和ASM)的底层原理,结合JVMTI接口,系统性地呈现了动态代码插桩、运行时行为监控等关键技术手段。同时简单的延伸至RASP的实践框架及内存马,以及在实时威胁检测与攻击防御中的创新应用。
该文为本人在学习字节码相关技术过程中的一点记录与思考,如有有问题的地方欢迎师傅们指出。
参考:
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
1
人收藏
2
人喜欢
转载
分享
0
条评论
某人
表情
可输入
255
字
评论
没有评论
发布投稿
热门文章
1
2025ISCC练武区域赛和决赛pwn以及擂台pwn合集
2
通过Elastic EDR看smbexec并进行二次开发Bypass
3
php代码审计篇 - 信呼OA 前台注入分析一
4
D3CTF-d3kshrm(预期&非预期)题解
5
Tomcat解析XML引入的新颖webshell构造方式
近期热点
一周
月份
季度
1
2025ISCC练武区域赛和决赛pwn以及擂台pwn合集
2
通过Elastic EDR看smbexec并进行二次开发Bypass
3
php代码审计篇 - 信呼OA 前台注入分析一
4
D3CTF-d3kshrm(预期&非预期)题解
5
Tomcat解析XML引入的新颖webshell构造方式
暂无相关信息
暂无相关信息
优秀作者
1
T0daySeeker
贡献值:28700
2
一天
贡献值:19800
3
1674701160110592
贡献值:18000
4
1174735059082055
贡献值:15000
5
Yale
贡献值:14000
6
LeeH
贡献值:10000
7
MeteorKai
贡献值:9000
8
姓*户
贡献值:8600
9
Arcueid
贡献值:8000
10
熊猫正正
贡献值:8000
目录
1、字节码及增强技术
1.1、什么是字节码
1.2、字节码增强技术
1.2.1、Javassist
1.2.2、ASM
1.2.3、遗留问题
2、Java Agent
2.1、什么是 Java Agent?
2.2、Java Agent的实现及使用
2.2.1 premain
2.2.2 agentmain
2.3、动态修改字节码
2.4、JVMTI
3、JavaAgent安全上的应用
3.1、RASP
3.2、JavaAgent内存马
4、总结
转载
标题
作者:
你好
http://www.a.com/asdsabdas
文章
转载
自
复制到剪贴板
没有评论