摘要

随着 Java 平台能力不断增强,Java 已不再只是传统意义上的“托管语言”。JDK 引入 Foreign Function & Memory API(以下简称 FFM API)后,Java 应用可以在不编写 JNI 代码、不额外加载本地动态库的情况下,与本地函数和堆外内存进行交互。这一能力提升了 Java 与 Native 生态的集成效率,但也带来了新的安全边界问题。

在传统 Java 命令执行防护体系中,RASP、WAF 和审计工具通常重点关注 Runtime.exec()ProcessBuilder.start()ProcessImpl 等调用链。然而,FFM API 提供了另一类 Native 调用路径,使得部分安全产品原有的 Java 层 Hook 策略可能出现覆盖盲区。本文从原理、风险、检测和防御四个角度,对 FFM API 相关安全问题进行分析,重点讨论如何从 JVM 参数控制、RASP Hook 扩展、主机行为监控和 WAF 规则更新等方面完善防护体系。

免责声明:本文仅用于安全研究、风险分析和授权场景下的防御验证,不提供可直接用于未授权攻击的利用代码或完整攻击链。请勿将相关技术用于任何非法用途。

关键词

Java 安全、FFM API、JDK 21、JDK 22、RASP、SpEL 注入、Native 调用、eBPF、JVM 安全


一、背景:Java Native 能力带来的安全边界变化

Java 命令执行相关风险长期存在。无论是反序列化漏洞、表达式注入、模板注入,还是配置错误导致的脚本执行,最终经常会落到几个典型 API 上:

  • java.lang.Runtime.exec()
  • java.lang.ProcessBuilder.start()
  • 底层 ProcessImpl 相关调用

因此,很多 RASP 产品会围绕这些入口进行 Hook,对命令参数、调用栈、上下文来源和进程行为进行审计或阻断。

这种防护模式在传统场景下有效,因为攻击链通常需要经过 Java 标准进程创建 API。然而,FFM API 的出现改变了这一前提。

FFM API 的设计目标是替代传统 JNI,降低 Java 调用 Native 函数的成本。开发者可以直接通过 Java 代码查找本地符号、构造函数描述符、分配堆外内存,并通过 MethodHandle 调用本地函数。

从安全视角看,这意味着:

  • Native 调用不一定经过 RuntimeProcessBuilder
  • 本地函数调用可以在 JVM 进程内完成;
  • 部分行为从 Java API 层下沉到 JVM 和操作系统层;
  • 传统基于字节码插桩的 RASP 需要更新检测边界。

二、FFM API 的核心组件

FFM API 主要位于 java.lang.foreign 包中,几个核心组件如下。

组件 作用 安全关注点
Linker Java 与本地代码之间的桥梁 获取 Native 调用能力的关键入口
SymbolLookup 查找已加载共享库中的函数符号 可能用于定位敏感 Native 函数
FunctionDescriptor 描述本地函数参数和返回值 决定 Java 与 Native 函数的调用约定
MemorySegment 表示连续内存区域 可用于传递参数或操作堆外内存
Arena 管理内存生命周期 控制堆外内存分配与释放
ValueLayout 描述基础数据类型内存布局 与 Native 函数签名匹配相关

一个典型的 FFM Native 调用过程可以抽象为以下几步:

  1. 获取平台链接器;
  2. 查找目标 Native 函数符号;
  3. 构造函数描述符;
  4. 创建下行调用句柄;
  5. 分配参数内存;
  6. 通过 MethodHandle 发起调用。

这里最值得安全人员关注的是:整个过程可以不经过传统的 Java 命令执行 API,而是通过 JVM 内部机制完成 Java 到 Native 的调用转换。


三、FFM API 与 JNI 的差异

很多人会将 FFM API 与 JNI 进行类比,但二者在安全审计上的差异非常明显。

对比项 JNI FFM API
开发方式 需要编写 C/C++ 并编译动态库 纯 Java 代码即可完成
文件依赖 通常需要 .so.dll 等文件 可直接调用系统已加载库
审计特征 System.loadLibrary() 等调用较明显 可能不触发传统库加载检测
部署成本 需要跨平台编译 与 Java 代码集成更紧密
RASP 可见性 可以关注库加载和 JNI 调用 需要额外覆盖 FFM 入口

从防御角度看,JNI 的风险较容易通过“动态库落盘”“库加载行为”“本地方法注册”等特征发现。而 FFM API 更加轻量,且调用链更贴近 JVM 内部执行机制,因此需要新的检测思路。


四、风险场景一:绕过传统 Java 命令执行 Hook

传统 RASP 对命令执行的检测通常集中在以下路径:

Runtime.exec()
    -> ProcessBuilder.start()
        -> ProcessImpl
            -> fork / execve

在这个路径中,RASP 可以在 Java 层较早介入,例如在 Runtime.exec()ProcessBuilder.start() 触发时进行拦截。

但 FFM API 的调用路径并不相同。其抽象路径更接近:

Linker
    -> SymbolLookup
        -> downcallHandle
            -> JVM adapter
                -> Native function

从 Java 层看,它可能只是一次 MethodHandle.invoke() 调用;从操作系统层看,最终行为可能表现为进程创建、内存映射或其他系统调用。

这就带来一个核心问题:

如果 RASP 只关注 RuntimeProcessBuilder 等传统入口,就可能无法覆盖 FFM API 引入的新 Native 调用路径。

因此,防护体系不能只停留在“拦截 Java 命令执行 API”这一层,而应进一步关注 Native 调用入口和系统调用行为。


五、风险场景二:表达式注入中的新型调用链

在 Java Web 应用中,SpEL 表达式注入是一类典型高危漏洞。如果开发者将用户输入直接拼入表达式并执行,攻击者就可能借助表达式能力访问 Java 类型、调用方法或构造对象。

传统 SpEL RCE 风险往往围绕以下敏感关键字展开:

  • Runtime
  • exec
  • ProcessBuilder
  • getRuntime

因此,WAF 和 RASP 常对这些关键字进行匹配或语义分析。

但如果运行环境允许 FFM API,并且表达式上下文可以访问相关类型,那么风险关键词可能发生变化。例如:

  • Linker
  • SymbolLookup
  • FunctionDescriptor
  • MemorySegment
  • downcallHandle
  • java.lang.foreign

这意味着,仅仅检测 Runtimeexec 等传统特征已经不够。对于支持 JDK 21+、JDK 22+ 的 Java 应用,安全规则库需要及时补充 FFM API 相关特征。


六、风险场景三:堆外内存与可执行内存分配

FFM API 不仅可以调用本地函数,还可以操作堆外内存。如果应用通过 Native 能力申请具备执行权限的内存区域,再写入机器码并执行,就可能形成更隐蔽的攻击路径。

从主机层看,此类行为的关键特征不是 Java API 调用,而是系统调用层面的异常行为,例如:

  • Java 进程申请可执行内存;
  • 出现 mmap 搭配 PROT_EXEC
  • 出现 mprotect 将内存页修改为可执行;
  • JVM 进程内出现异常代码执行行为。

正常情况下,Java 应用很少需要自行申请 RWX 权限内存。JIT 编译器确实会管理代码缓存,但其行为模式与一次性申请可读、可写、可执行内存的攻击行为存在差异。

因此,对这类风险,单纯依赖 Java 层 Hook 效果有限,更适合通过 eBPF、EDR、审计日志等方式从主机侧进行监控。


七、检测思路一:JVM 参数层面的限制

1. JDK 21 场景

在 JDK 21 中,FFM API 仍属于 Preview 能力,通常需要显式启用 Preview 特性。生产环境应避免随意开启相关参数,尤其是对外暴露的 Web 应用、网关服务、管理后台和任务调度平台。

建议:

# 生产环境原则上不启用
--enable-preview

如果业务确实依赖 Preview 特性,应进行专项安全评估,并配合 RASP、主机监控和依赖审计。

2. JDK 22 及以上场景

在更高版本 JDK 中,FFM API 逐步正式化。此时更应关注 Native Access 的授权范围,避免所有模块默认拥有 Native 调用能力。

建议遵循最小权限原则:

--enable-native-access=<明确允许的模块>

不建议在生产环境中使用过宽的授权方式,尤其要避免将 Native Access 开放给不可信模块、插件系统、表达式执行模块或脚本引擎模块。


八、检测思路二:RASP Hook 覆盖范围扩展

传统 RASP 应继续保留对以下入口的检测:

  • Runtime.exec()
  • ProcessBuilder.start()
  • ProcessImpl
  • 反射调用命令执行 API
  • 脚本引擎调用链

同时,应新增对 FFM API 关键入口的检测。

Hook 点 建议关注内容
Linker.nativeLinker() 是否获取 Native 链接能力
SymbolLookup.find() 是否查找敏感 Native 符号
Linker.downcallHandle() 是否创建 Native 调用句柄
SymbolLookup.libraryLookup() 是否加载额外本地库
MemorySegment 写操作 是否写入异常二进制内容
Arena 分配 是否频繁分配堆外内存

敏感符号可以重点关注:

  • system
  • execve
  • fork
  • popen
  • mmap
  • mprotect
  • dlopen
  • dlsym

需要注意的是,不能简单地“一刀切”拦截所有 FFM API 调用。部分高性能组件、数据库驱动、向量计算库或系统集成模块可能存在合理使用场景。因此建议结合调用栈、模块名、业务上下文和运行环境进行综合判断。


九、检测思路三:基于 eBPF 的主机行为监控

对于 Native 调用和内存执行类风险,主机层监控非常关键。可以通过 eBPF 监控 Java 进程的敏感系统调用行为。

例如,重点关注以下事件:

  • Java 进程调用 execve
  • Java 进程调用 mmap 且带有 PROT_EXEC
  • Java 进程调用 mprotect 将内存修改为可执行;
  • Java 进程产生异常子进程;
  • Java 服务进程访问异常共享库路径。

一个防御侧监控思路如下:

# 示例:监控进程申请可执行内存的行为
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_mmap
/args->prot & 4/
{
    printf("[ALERT] PID=%d COMM=%s mmap(len=%lu, prot=0x%x)\n",
           pid, comm, args->len, args->prot);
}'

该类规则适合部署在测试环境、灰度环境或高安全区域中,用于发现异常 Native 行为。

在生产环境中,建议结合白名单策略,避免对 JVM 自身 JIT、性能分析工具或合法 Native 组件产生误报。


十、检测思路四:WAF 与网关规则更新

如果 Web 应用存在表达式执行、模板渲染、脚本执行、动态规则执行等能力,则 WAF 不应只检测传统命令执行关键字,还应增加 FFM API 相关特征。

可关注的关键字包括:

java.lang.foreign
nativeLinker
downcallHandle
SymbolLookup
FunctionDescriptor
MemorySegment
Arena
ValueLayout

同时应结合上下文判断,例如:

  • 参数中是否出现 Java 类型引用;
  • 是否存在表达式语法;
  • 是否出现 Native 函数名称;
  • 是否存在异常编码、拼接或分段绕过;
  • 是否命中高危接口路径。

示例规则思路:

SecRule ARGS "@rx (?i)(nativeLinker|downcallHandle|SymbolLookup|java\.lang\.foreign|FunctionDescriptor|MemorySegment)" \
    "id:100001,\
     phase:2,\
     deny,\
     status:403,\
     msg:'Potential FFM API abuse attempt',\
     tag:'attack-rce'"

实际部署中,应根据业务误报情况进行灰度验证,并结合日志审计持续优化。


十一、开发侧治理建议

1. 禁止直接执行不可信表达式

对于 SpEL、OGNL、MVEL、Groovy、JavaScript 引擎等能力,应避免直接执行用户输入。

错误做法:

用户输入 -> 拼接表达式 -> 直接求值

推荐做法:

用户输入 -> 白名单字段解析 -> 参数化处理 -> 固定规则执行

2. 对表达式上下文做最小化暴露

如果业务必须使用表达式引擎,应限制表达式可访问的类型、方法和对象。

建议:

  • 禁止访问任意 Java 类型;
  • 禁止反射能力;
  • 禁止访问 ClassLoader;
  • 禁止访问系统属性;
  • 禁止访问 java.lang.foreign
  • 禁止访问进程、文件、网络相关 API。

3. 建立 JDK 升级安全评估机制

JDK 升级不仅是兼容性问题,也是安全边界变化问题。每次从 JDK 8、11、17 升级到 21 或更高版本时,应评估:

  • 是否启用 Preview 特性;
  • 是否开放 Native Access;
  • 是否引入新的外部函数调用能力;
  • RASP 是否支持新 JDK 特性;
  • WAF 规则是否覆盖新增风险关键字;
  • 主机监控是否能识别 Native 行为。

十二、企业落地建议

对于企业级 Java 应用,可以从以下几个层面形成闭环治理。

1. 资产梳理

梳理所有使用 JDK 21+ 的应用,重点关注:

  • 对外暴露的 Web 服务;
  • 存在表达式执行能力的系统;
  • 规则引擎、流程引擎、低代码平台;
  • 插件化系统;
  • 运维管理平台;
  • 自动化执行平台。

2. 参数基线

建立 JVM 启动参数安全基线,重点检查:

  • 是否启用 --enable-preview
  • 是否使用过宽的 --enable-native-access
  • 是否加载未知 Agent;
  • 是否存在异常本地库路径;
  • 是否开启危险调试参数。

3. RASP 升级

推动 RASP 能力从传统 Java API Hook 扩展到:

  • FFM API 关键入口;
  • 反射调用链;
  • 表达式执行链;
  • Native 符号查找;
  • 异常内存权限变更;
  • Java 进程异常子进程行为。

4. 主机检测

结合 EDR、HIDS、eBPF 等能力,重点监控:

  • Java 进程创建 shell;
  • Java 进程执行系统工具;
  • Java 进程申请可执行内存;
  • Java 进程加载异常共享库;
  • Java 进程出现异常网络连接。

5. 安全测试

在授权测试环境中构造安全验证用例,验证以下能力是否有效:

  • WAF 是否拦截 FFM 关键字;
  • RASP 是否识别 Native 调用;
  • 主机侧是否捕获异常系统调用;
  • 告警是否能关联到应用、接口和调用栈;
  • 阻断策略是否影响正常业务。

十三、总结

FFM API 是 Java 平台能力演进的重要方向,它让 Java 与 Native 生态的交互更加高效,也让 Java 在系统级编程场景中具备更强能力。但从安全角度看,任何新的 Native 交互能力都可能改变原有防护边界。

传统 Java 安全防护往往聚焦在 Runtime.exec()ProcessBuilder、反射和类加载等路径上。但在 FFM API 场景下,Native 调用可能通过新的链路完成,导致仅基于传统 Java API 的 RASP Hook 出现覆盖不足。

因此,防御体系需要从单点拦截转向多层联动:

  • JVM 层限制不必要的 Native Access;
  • RASP 层扩展 FFM API Hook;
  • WAF 层更新关键字和语义规则;
  • 主机层基于 eBPF/EDR 监控系统调用;
  • 开发侧避免执行不可信表达式;
  • 企业侧建立 JDK 升级安全评估机制。

安全的本质不是阻止技术演进,而是在能力演进的同时同步更新边界认知。FFM API 的出现提醒我们:Java 应用安全不能只看 Java 层 API,更要关注 JVM 与操作系统之间的真实行为。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐