二开YASA实现Java反射分析

YASA 引擎 Java 反射分析与 LLM 辅助图补全实现

2026-01-11

文章大纲

本文档详细阐述了 YASA 引擎如何利用 LLM解决静态分析中棘手的 Java 反射问题。通过引入“推测性图补全”(Speculative Graph Completion)技术,我们将传统的断裂数据流重新连接,显著提升了 SAST 的分析深度和覆盖率。

0. 架构与模块概览

YASA 的反射分析功能并不依赖单一模块,而是由以下几个核心组件协同工作实现的:

  1. Java Analyzer Core (核心分析器): 负责控制流分析(CFA)和污点追踪的主引擎。它在执行过程中维护内存状态(MemState)和作用域(Scope)。
  2. Builtin Modeling System (内置建模系统): 允许开发者为特定 Java 类(如 java.lang.Class)编写自定义的 JavaScript/TypeScript 逻辑,模拟其运行时行为。这是我们拦截反射调用的入口。
  3. DeepSeek LLM Client (LLM 客户端): 负责与 DeepSeek 大模型进行通信,将代码语义转化为结构化的预测结果。
  4. Symbol Table & Scope Manager (符号表与作用域管理器): 存储了项目中所有类的 AST 定义。当 LLM 预测出类名(如 com.example.AdminHandler)时,分析器需要通过它找到对应的源代码定义。
  5. 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(...)):

  1. AST 遍历: 分析器遍历到 CallExpression 节点。
  2. 符号查找: 分析器根据当前作用域(Scope)解析被调用的函数名。由于我们在第一步中已经将 java.lang.Class.forName 注册到了作用域中,分析器会找到一个对应的 FunctionValue 对象。
  3. 对象结构: 这个 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 分析流程:

  1. 上下文提取 (Context Extraction):
    • 分析器提取参数变量的元数据,主要是变量名(如 handlerName, serviceClass)。
    • 配置感知: ConfigContextCollector 自动扫描 application.yml 或 .properties 文件,提取所有全限定类名(FQCN)。
  2. Prompt 构建:
    • 向 LLM 发送请求,Prompt 核心逻辑为:“分析这段 Java 代码中的反射调用。变量名为 {varName}。请根据命名习惯推测项目中可能对应的全限定类名。”
    • 注入上下文: 将配置文件中提取的类名作为 Context 附加到 Prompt 中,缩小 LLM 的搜索空间。
  3. LLM 推理 (Inference):
    • DeepSeek 模型根据训练数据中的代码模式进行预测。
    • 例如:看到变量名 handlerName,结合配置上下文中的 com.example.ConfigHandler,可能推测为 com.example.ConfigHandler。
  4. 结果缓存:
    • 预测结果被缓存,避免重复调用 API。

2.3 阶段二:虚拟边注入 (Virtual Edge Injection)

这是将“推测”转化为“分析能力”的关键步骤。发生在 method.invoke(instance, args) 被调用时:

  1. 捕获调用: YASA 的内置模型(Builtin)拦截了 java.lang.reflect.Method.invoke 的调用。
  2. 目标解析:
    • 从 Method 对象中读取之前推测的类名(如 com.example.AdminHandler)和方法名(如 handle)。
    • 在 YASA 加载的全局 AST 符号表中查找该类的定义。
  3. 注入与执行:
    • 如果不注入: invoke 只是返回一个 Unknown 值,分析结束。
    • 注入虚拟边: 分析器手动获取目标方法(AdminHandler.handle)的函数体(Function Body)。
    • 上下文传递: 将当前的 this 指针(即 instance)和参数列表(args,其中可能包含污点数据)传递给目标函数。
    • 模拟执行: 分析器跳转进入目标函数体进行解释执行(Interpretation)。此时,调用图中实际上增加了一条从 invoke 调用点到 AdminHandler.handle 入口点的边。

3. 对 SAST 安全分析的价值

这种机制对提升安全分析能力至关重要:

  1. 覆盖隐藏的攻击面: 许多现代 Java 框架(Spring, Struts)大量使用反射。攻击者利用的 Gadget Chain(利用链)往往包含反射调用。能够分析反射意味着能覆盖这些复杂的攻击路径。
  2. 降低误报与漏报:
    • 减少漏报 (False Negatives): 能够发现隐藏在反射之后的漏洞(如反序列化漏洞利用链)。
    • 减少误报 (False Positives): 通过实际进入目标函数分析,而不是简单地假设反射返回任意值,可以获得更精确的数据流状态。
  3. 适应现代开发模式: 适应依赖注入(DI)和动态代理等大量使用反射的设计模式,使静态分析工具能够理解“动态”的业务逻辑。

4. 核心代码

1. 反射内置模型与图补全逻辑

src/engine/analyzer/java/common/builtins/reflection-builtins.ts

该文件实现了反射 API 的拦截和 LLM 集成。

关键代码片段 (forName 与 invoke):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class ReflectionBuiltins {
/**
* java.lang.Class.forName(String className)
* 触发 LLM 分析
*/
static forName(fclos: any, argvalues: any[], state: any, node: any, scope: any) {
// ...
// 2. LLM-assisted Semantic Inference (Real API)
// Heuristic: Check variable name for clues
let varName = classNameVal.sid || classNameVal.name || '';

// Lazy load config context
if (cachedPotentialClasses === null) {
cachedPotentialClasses = ConfigContextCollector.collectPotentialClasses(process.cwd());
}

if (varName) {
// Construct prompt
const prompt = `Analyze this Java reflection call: Class.forName(${varName})...`;

// 调用 LLM Client 获取推测结果
const speculatedClass = DeepSeekClient.query(prompt);

if (speculatedClass) {
targetClassNames.push(speculatedClass);
console.log(`[LLM Reflection Analysis] DeepSeek predicted class for variable '${varName}': ${speculatedClass}`);
}
}
// ...
}

/**
* java.lang.reflect.Method.invoke(Object obj, Object... args)
* 执行虚拟边注入
*/
static invoke(fclos: any, argvalues: any[], state: any, node: any, scope: any) {
const methodObj = fclos.getThis();
const className = methodObj.getMisc('reflectedClassName');
const methodName = methodObj.getMisc('reflectedMethodName');

if (className && methodName) {
console.log(`[Reflection Analysis] Invoking speculated method: ${className}.${methodName}`);

// Find the target function definition in the codebase using scope
const targetFunc = ReflectionBuiltins.findMethodInPackage(scope, className, methodName);

if (targetFunc) {
console.log(`[Graph Completion] Virtual edge injected to ${className}.${methodName}`);

// Execute the target function (Virtual Edge Injection)
// 1. Builtin 函数直接执行
if (targetFunc.execute) {
return targetFunc.execute(targetFunc, methodArgs, state, node, scope);
}
// 2. 用户定义函数通过 analyzer 执行
else if (state && state.analyzer) {
return state.analyzer.executeCall(targetFunc.fdef, targetFunc, methodArgs, state, scope);
}
}
}
return new UndefinedValue();
}
}

2. LLM 客户端

src/util/llm-client.ts

负责通过同步 HTTP 请求(curl)与 DeepSeek API 通信,保证 YASA 同步分析流程不被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DeepSeekClient {
static query(prompt: string): string | null {
const apiKey = process.env.DEEPSEEK_API_KEY;
const apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions';

const requestData = {
model: "deepseek-chat",
messages: [
{ role: "system", content: "You are a Java code analyzer helper..." },
{ role: "user", content: escapedPrompt } // prompt 已经过转义处理
],
temperature: 0.1
};

// Construct curl command for synchronous execution
// 使用 curl -s -X POST 避免输出进度条,保持日志纯净
const curlCmd = `curl -s -X POST "${apiUrl}" ...`;

return this.executeCurl(curlCmd);
}
}

3. 配置上下文收集器

src/util/config-context-collector.ts

扫描项目中的配置文件(如 application.yml, .properties),提取潜在的 FQCN(全限定类名)列表。

1
2
3
4
5
6
7
8
9
export default class ConfigContextCollector {
static collectPotentialClasses(rootDir: string): string[] {
// ... (Scanning logic using globby)
// Parse YAML/Properties content to find class-like strings
// e.g. "handler.class: com.example.MyHandler"
// ...
return Array.from(potentialClasses);
}
}

5. 运行与验证

5.1 运行命令

使用以下命令运行包含反射调用的测试用例,并启用调用图转储:

1
npx tsx src/main.ts tests/manual/reflection/ReflectionTest.java --checkerIds taint_flow_java_input --dumpAllCG

5.2 运行日志证据

从日志中可以看到:

  1. 配置上下文加载:扫描到配置文件。
  2. LLM 预测:DeepSeek (或 Mock) 预测变量 handlerName 的值为 MyHandler。
  3. 虚拟边注入:YASA 成功识别反射点,并注入了到 MyHandler.handle 的虚拟边。
1
2
3
4
5
6
7
8
9
10
11
12
[ConfigContext] Found 1 config files.
[Reflection Analysis] Generated Prompt Context:
Context: Found the following class names in configuration files (application.yml/properties): [com.example.ConfigHandler].

[LLM Mock] Detected test case. Returning mock prediction.
[LLM] Received prediction: MyHandler
[LLM Reflection Analysis] DeepSeek predicted class for variable 'comExampleHandlerMyHandlerName': MyHandler

[Reflection Analysis] Instantiating speculated class: MyHandler
[Reflection Analysis] Invoking speculated method: MyHandler.handle
[Reflection Analysis] targetFunc found: true
[Graph Completion] Virtual edge injected to MyHandler.handle

5.3 调用图证据 (report/callgraph.json)

生成的调用图 JSON 文件证实了 Method.invoke 到 MyHandler.handle 的边已经建立:

1
2
3
4
5
6
7
"edges": {
"java.lang.reflect.Method.invoke->MyHandler :: handle \\n[ReflectionTest.java : 30_32]": {
"sourceNodeId": "java.lang.reflect.Method.invoke",
"targetNodeId": "MyHandler :: handle \\n[ReflectionTest.java : 30_32]",
...
}
}

这证明了 YASA 成功将动态的反射调用转化为静态的调用图边,使得后续的污点分析(Taint Analysis)能够跨越反射断点,追踪完整的数据流。