本文档详细阐述了 YASA 引擎如何利用 LLM解决静态分析中棘手的 Java 反射问题。通过引入“推测性图补全”(Speculative Graph Completion)技术,我们将传统的断裂数据流重新连接,显著提升了 SAST 的分析深度和覆盖率。
0. 架构与模块概览
YASA 的反射分析功能并不依赖单一模块,而是由以下几个核心组件协同工作实现的:
- Java Analyzer Core (核心分析器): 负责控制流分析(CFA)和污点追踪的主引擎。它在执行过程中维护内存状态(MemState)和作用域(Scope)。
- Builtin Modeling System (内置建模系统): 允许开发者为特定 Java 类(如
java.lang.Class)编写自定义的 JavaScript/TypeScript 逻辑,模拟其运行时行为。这是我们拦截反射调用的入口。 - DeepSeek LLM Client (LLM 客户端): 负责与 DeepSeek 大模型进行通信,将代码语义转化为结构化的预测结果。
- Symbol Table & Scope Manager (符号表与作用域管理器): 存储了项目中所有类的 AST 定义。当 LLM 预测出类名(如
com.example.AdminHandler)时,分析器需要通过它找到对应的源代码定义。 - Config Context Collector (配置上下文收集器): 扫描项目配置文件(如
application.yml),提取潜在的类名列表,辅助 LLM 进行更准确的预测。
1. 背景与演进:反射分析的痛点
1.1 传统 SAST 的困境 (Before)
在引入 LLM 之前,YASA(以及大多数静态分析工具)处理反射调用时面临巨大的挑战:
- 数据流中断: 当分析器遇到
Class.forName(variable)时,如果variable不是硬编码的字符串常量(例如来自配置文件或用户输入),分析器无法知道加载了哪个类。 - 调用图断裂: 随后的
method.invoke(obj, args)调用变成了“黑盒”。分析器不知道控制流去了哪里。 - 安全漏报: 假如污点数据(Tainted Data)作为参数传入
invoke,由于不知道目标方法,污点追踪在此中断。如果目标方法内部存在 SQL 注入或命令执行漏洞,SAST 将无法检测到。
图示 (Before):
graph LR
A[Source: HTTP Request] --> B[Data Flow]
B --> C["Class.forName(?)"]
C --> D["Method.invoke(?)"]
D -.->|断路| X((Unknown Target))
style X fill:#f9f,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
style D fill:#ffcccc,stroke:#333
1.2 LLM 赋能后的新范式 (After)
引入 LLM 后,YASA 不再因为不确定性而停止分析,而是进行合理的推测:
- 语义理解: 利用 LLM 强大的语义推理能力,根据变量名、上下文代码风格、配置文件等信息,推断动态变量背后最可能的类名。
- 虚拟边注入: 基于推测结果,在调用图中强制插入一条指向目标方法的边(Virtual Edge)。
- 全链路追踪: 污点分析现在可以跨过反射调用,继续在目标方法内部追踪,发现深层漏洞。
图示 (After):
graph LR
A[Source: HTTP Request] --> B[Data Flow]
B --> C["Class.forName(var)"]
C --"LLM推测: AdminHandler"--> D["Method.invoke(obj, args)"]
D == "注入虚拟边 (Virtual Edge)" ==> E["Target: AdminHandler.handle()"]
E --> F[Sink: SQL Injection]
style C fill:#e1f5fe,stroke:#01579b
style D fill:#e1f5fe,stroke:#01579b
style E fill:#e8f5e9,stroke:#2e7d32
style F fill:#ffebee,stroke:#c62828
2. 技术实现详解
2.1 核心机制:YASA 如何 Hook 反射调用 (Core Hook Mechanism)
YASA 将反射调用 Hook 到自定义 Builtin 实现的机制主要包含三个阶段:静态配置映射、运行时符号解析 和 执行分发。
其核心原理是:在分析器初始化时,将 Java 的标准库类(如 java.lang.Class)映射到 YASA 内部定义的 JavaScript/TypeScript 对象(FunctionValue),并为这些对象挂载一个特殊的 execute 属性。当分析器执行到对应的方法调用时,会优先检查并执行这个 execute 函数,从而跳出通用的 AST 分析循环,进入我们自定义的逻辑。
1. 静态配置与加载 (Initialization)
YASA 启动时,JavaInitializer 会读取配置文件,建立 Java 类与 Builtin 实现文件的映射关系。
- 配置文件:
class-hierarchy-and-modeling.json定义了哪些类需要被 Hook。1
2
3
4
5
6
7
8{
"java.lang.Class": {
"modelingFilePath": "./builtins/reflection-builtins"
},
"java.lang.reflect.Method": {
"modelingFilePath": "./builtins/reflection-builtins"
}
} - 加载逻辑:
java-initializer.ts中的initCommonGlobalBuiltin方法会读取上述配置,通过require加载对应的modelingFilePath(例如reflection-builtins.ts),并将其中的静态方法注册到全局符号表(Symbol Table)或包作用域中。
2. 运行时拦截与符号解析 (Interception)
当 YASA 的分析引擎(JavaAnalyzer)在分析源代码时遇到函数调用(例如 Class.forName(...)):
- AST 遍历: 分析器遍历到
CallExpression节点。 - 符号查找: 分析器根据当前作用域(Scope)解析被调用的函数名。由于我们在第一步中已经将
java.lang.Class.forName注册到了作用域中,分析器会找到一个对应的FunctionValue对象。 - 对象结构: 这个
FunctionValue对象在内存中大概是这样的:1
2
3
4
5
6{
id: "forName",
qid: "java.lang.Class.forName",
// 关键点:这个对象有一个 execute 属性,指向 reflection-builtins.ts 中的 forName 函数
execute: ReflectionBuiltins.forName
}
3. 执行分发 (Dispatch)
这是 Hook 生效的关键步骤,发生在通用分析器基类 analyzer.ts 中。
- 入口:
processCallExpression调用executeCall,最终进入executeSingleCall。 - 检测 Builtin: 在
executeSingleCall方法中,分析器检查当前的函数对象(fclos)是否有execute属性且没有fdef(源代码定义)。1
2
3
4
5
6
7
8
9// src/engine/analyzer/common/analyzer.ts
if (!fdecl) {
if (!fclos.execute) {
// 普通函数处理...
}
// 标记为需要执行 Builtin
execute_builtin = true
} - 触发执行: 如果
execute_builtin为true,分析器直接通过call调用该函数,将控制权移交给我们自定义的 TypeScript 代码:1
2// src/engine/analyzer/common/analyzer.ts L2563
return_value = fclos.execute.call(this, fclos, argvalues, new_state, node, scope)
通过这种机制,YASA 实现了对标准库方法的替换。当分析器遇到 Method.invoke 时,它实际上执行的是 reflection-builtins.ts 中的 invoke 函数。在这个函数里,我们才可以自由地调用 LLM 进行推测、查找目标方法的 AST 以及注入虚拟边。
2.2 阶段一:LLM 辅助的目标类发现 (Target Discovery)
当分析器执行到 Class.forName(arg) 且 arg 为非字面量时,触发 LLM 分析流程:
- 上下文提取 (Context Extraction):
- 分析器提取参数变量的元数据,主要是变量名(如
handlerName,serviceClass)。 - 配置感知:
ConfigContextCollector自动扫描application.yml或.properties文件,提取所有全限定类名(FQCN)。
- 分析器提取参数变量的元数据,主要是变量名(如
- Prompt 构建:
- 向 LLM 发送请求,Prompt 核心逻辑为:“分析这段 Java 代码中的反射调用。变量名为
{varName}。请根据命名习惯推测项目中可能对应的全限定类名。” - 注入上下文: 将配置文件中提取的类名作为
Context附加到 Prompt 中,缩小 LLM 的搜索空间。
- 向 LLM 发送请求,Prompt 核心逻辑为:“分析这段 Java 代码中的反射调用。变量名为
- LLM 推理 (Inference):
- DeepSeek 模型根据训练数据中的代码模式进行预测。
- 例如:看到变量名
handlerName,结合配置上下文中的com.example.ConfigHandler,可能推测为com.example.ConfigHandler。
- 结果缓存:
- 预测结果被缓存,避免重复调用 API。
2.3 阶段二:虚拟边注入 (Virtual Edge Injection)
这是将“推测”转化为“分析能力”的关键步骤。发生在 method.invoke(instance, args) 被调用时:
- 捕获调用: YASA 的内置模型(Builtin)拦截了
java.lang.reflect.Method.invoke的调用。 - 目标解析:
- 从
Method对象中读取之前推测的类名(如com.example.AdminHandler)和方法名(如handle)。 - 在 YASA 加载的全局 AST 符号表中查找该类的定义。
- 从
- 注入与执行:
- 如果不注入:
invoke只是返回一个Unknown值,分析结束。 - 注入虚拟边: 分析器手动获取目标方法(
AdminHandler.handle)的函数体(Function Body)。 - 上下文传递: 将当前的
this指针(即instance)和参数列表(args,其中可能包含污点数据)传递给目标函数。 - 模拟执行: 分析器跳转进入目标函数体进行解释执行(Interpretation)。此时,调用图中实际上增加了一条从
invoke调用点到AdminHandler.handle入口点的边。
- 如果不注入:
3. 对 SAST 安全分析的价值
这种机制对提升安全分析能力至关重要:
- 覆盖隐藏的攻击面: 许多现代 Java 框架(Spring, Struts)大量使用反射。攻击者利用的 Gadget Chain(利用链)往往包含反射调用。能够分析反射意味着能覆盖这些复杂的攻击路径。
- 降低误报与漏报:
- 减少漏报 (False Negatives): 能够发现隐藏在反射之后的漏洞(如反序列化漏洞利用链)。
- 减少误报 (False Positives): 通过实际进入目标函数分析,而不是简单地假设反射返回任意值,可以获得更精确的数据流状态。
- 适应现代开发模式: 适应依赖注入(DI)和动态代理等大量使用反射的设计模式,使静态分析工具能够理解“动态”的业务逻辑。
4. 核心代码
1. 反射内置模型与图补全逻辑
src/engine/analyzer/java/common/builtins/reflection-builtins.ts
该文件实现了反射 API 的拦截和 LLM 集成。
关键代码片段 (forName 与 invoke):
1 | class ReflectionBuiltins { |
2. LLM 客户端
src/util/llm-client.ts
负责通过同步 HTTP 请求(curl)与 DeepSeek API 通信,保证 YASA 同步分析流程不被阻塞。
1 | class DeepSeekClient { |
3. 配置上下文收集器
src/util/config-context-collector.ts
扫描项目中的配置文件(如 application.yml, .properties),提取潜在的 FQCN(全限定类名)列表。
1 | export default class ConfigContextCollector { |
5. 运行与验证
5.1 运行命令
使用以下命令运行包含反射调用的测试用例,并启用调用图转储:
1 | npx tsx src/main.ts tests/manual/reflection/ReflectionTest.java --checkerIds taint_flow_java_input --dumpAllCG |
5.2 运行日志证据
从日志中可以看到:
- 配置上下文加载:扫描到配置文件。
- LLM 预测:DeepSeek (或 Mock) 预测变量
handlerName的值为MyHandler。 - 虚拟边注入:YASA 成功识别反射点,并注入了到
MyHandler.handle的虚拟边。
1 | [ConfigContext] Found 1 config files. |
5.3 调用图证据 (report/callgraph.json)
生成的调用图 JSON 文件证实了 Method.invoke 到 MyHandler.handle 的边已经建立:
1 | "edges": { |
这证明了 YASA 成功将动态的反射调用转化为静态的调用图边,使得后续的污点分析(Taint Analysis)能够跨越反射断点,追踪完整的数据流。