Agent型内存马其实就不局限于tomcat了,它是给运行中的JVM加载了一个agentmain-Agent,这个agentmain-Agent用javassist生成了木马的字节码,然后用Instrumentation把字节码加到类的方法里实现的攻击。理论上其他java程序也能用这种方法注入内存马。
Agent有两种,premain-Agent是在 JVM 启动前加载的,agentmain-Agent是JVM 启动之后加载的。
几种 Java Agent 实例
premain-Agent
这部分和内存马无关,可以直接看agentmain-Agent,但是很多程序的破解版是用这种方法实现的,可以了解一下
从官方文档中可知晓,首先我们必须实现 premain 方法,同时我们 jar 文件的清单(mainfest)中必须要含有 Premain-Class 属性
我们可在命令行利用 -javaagent 来实现启动时加载。
premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法
例如:
agent.java
1 | package org.example; |
agent.mf
1 | Manifest-Version: 1.0 |
hello.java
1 | package org.example; |
hello.mf
1 | Manifest-Version: 1.0 |
1 | # 在src/main/java下执行,不要在src/main/java/org/example下执行,jar打包时候路径不对 |
可以看到agent的代码先输出,然后才是hello的代码
agentmain-Agent
相较于 premain-Agent 只能在 JVM 启动前加载,agentmain-Agent 能够在JVM启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和 JVM 有关的两个类。
VirtualMachine类
com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、线程dump、类信息统计(例如JVM加载的类)等功能。
该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM 上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法
JAVA
1 | //允许我们传入一个JVM的PID,然后远程连接到该JVM上 |
VirtualMachineDescriptor 类
com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
JAVA
1 | package com.drunkbaby; |
实现agentmain-Agent
下面我们就来实现一个agentmain-Agent。首先我们编写一个 Sleep_Hello 类,模拟正在运行的 JVM
1 | package org.example; |
然后编写我们的agent类
1 | package org.example; |
agent.mf
1 | Manifest-Version: 1.0 |
打包成jar,和上面premain打包的方法一样,但是不用打包Sleep_Hello
1 | javac .\org\example\agent.java |
最后准备一个 Inject 类,将我们的 agent-main 注入目标 JVM:
1 | package org.example; |
然后依次运行Sleep_Hello和Inject_Agent,可以看到agent成功注入了:

Javassist
什么是 Javassist
Java 字节码以二进制的形式存储在 .class 文件中,每一个.class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过手动的方式去生成一个新的类对象。其使用方式类似于反射。
ClassPool
ClassPool是CtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放CtClass对象的容器。
获得方法: ClassPool cp = ClassPool.getDefault();。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
1 | cp.insertClassPath(new ClassClassPath(<Class>)); |
CtClass
可以将其理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以ClassPool.get(ClassName)中获取。
CtMethod
同理,可以理解成加强版的Method对象。可通过CtClass.getDeclaredMethod(MethodName)获取,该类提供了一些方法以便我们能够直接修改方法体
JAVA
1 | public final class CtMethod extends CtBehavior { |
传递给方法 insertBefore() ,insertAfter() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
| 标识符 | 含义 |
|---|---|
| $0, $1, $2, ... | this 和实参 |
| $args | 参数数组。$args 的类型是 Object[] |
| $$ | 所有实参,例如m($$)等同于m($1,$2,...) |
| $cflow(...) | cflow变量 |
| $r | 返回值类型。用于强制类型转换表达式 |
| $w | 包装类型。用于强制类型转换表达式 |
| $_ | 结果值 |
| $sig | java.lang.Class对象的数组,表示参数类型 |
| $type | java.lang.Class对象的数组,表示结果类型 |
| $class | java.lang.Class对象的数组,表示当前被编辑的类 |
使用示例
pom.xml
XML
1 | <dependencies> |
创建测试类
JAVA
1 | package org.example; |
生成的Person.class反编译后如下
1 | // |
Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
其在 Java 中是一个接口,常用方法如下
JAVA
1 | public interface Instrumentation { |
ClassFileTransformer
转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。
JAVA
1 | //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。 |
替换目标 JVM 已加载类
下面我们简单实现一个能够替换目标 JVM 已加载类的 agentmain-Agent
Sleep_Hello.java,要被替换的类
1 | package org.example; |
agent.java,要加载的agent
1 | package org.example; |
Hello_Transform.java,要加载的transformer,包含具体如何修改Sleep_Hello的代码
1 | package org.example; |
MANIFEST.MF,放在src/main/resources/META-INF/MANIFEST.MF
1 | Manifest-Version: 1.0 |
Inject_Agent.java,将agent注入到Sleep_Hello中
1 | package org.example; |
用插件打包吧,javassist是第三方库不好打包,pom.xml加入:
1 | <build> |
assembly:assembly打包,输出在target下:

这个就是Inject_Agent.java里loadAgent的路径,必须是with-dependencies,第一个包里MANIFEST不对
然后运行Sleep_Hello再运行Inject_Agent就能替换Sleep_Hello中的hello方法

当然直接打包也行,得先把javassist-3.29.2-GA.jar复制到源码路径,然后用-cp指定路径
1 | javac -cp .\javassist-3.29.2-GA.jar .\Hello_Transform.java .\agent.java |
改一下Inject_Agent里loadAgent的路径,效果是一样的:

大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义
Instrumentation#redefineClasses方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是 private static/final 修饰的
- 可以修改方法体
Agent 内存马
原理就是用上面的方法替换tomcat里的方法,放一个马进去
但在实际尝试的时候报错:java.lang.RuntimeException: org.apache.catalina.core.ApplicationFilterChain class is frozen
Tomcat 在启动时会将
ApplicationFilterChain类“冻结”(frozen),以防止在运行时被修改。
其实是可以注入的,报错不影响执行
尝试servlet型内存马修改自己servlet的service方法,虽然报错,但是能执行:
Inject_Agent.java
1 | package org.example; |
agent.java
1 | package org.example; |
Filter_Transform.java
1 | package org.example; |
MANIFEST.MF
1 | Manifest-Version: 1.0 |

filter型内存马也是可以注入的,之前以为不能注入是因为用的payload只执行不回显
Inject_Agent.java
1 | package org.example; |
agent.java
1 | package org.example; |
Filter_Transform.java
1 | package org.example; |
MANIFEST.MF
1 | Manifest-Version: 1.0 |