基于Context上下文驱动的Android 恶意软件检测实现
大型语言模型(LLMs)凭借其出色的零样本推理能力和强大的语义理解能力,为恶意软件检测领域带来了新的希望。然而,将 LLMs 直接应用于 Android 恶意软件检测时,却面临着两大核心挑战:一是 Android 应用中海量的支持代码远超 LLMs 的上下文窗口限制,使得恶意行为在良性功能中难以被识别;二是 Android 应用复杂的程序结构和组件间依赖关系,超出了 LLMs 基于序列的推理能力,
传统 Android 恶意软件检测方法在面对不断演化的攻击手段、数据集偏差以及缺乏可解释性等问题时,逐渐显得力不从心。
近年来,大型语言模型(LLMs)凭借其出色的零样本推理能力和强大的语义理解能力,为恶意软件检测领域带来了新的希望。然而,将 LLMs 直接应用于 Android 恶意软件检测时,却面临着两大核心挑战:一是 Android 应用中海量的支持代码远超 LLMs 的上下文窗口限制,使得恶意行为在良性功能中难以被识别;二是 Android 应用复杂的程序结构和组件间依赖关系,超出了 LLMs 基于序列的推理能力,导致代码分析碎片化,无法准确推断恶意意图。
为解决这些难题,提出了一种创新的上下文驱动框架,通过关键上下文提取和分层代码推理机制,充分释放 LLMs 在 Android 恶意软件检测中的潜力,实现高效、可解释的恶意软件检测。本文将详细拆解该框架的核心技术细节,分享其实现思路与实践效果。
一、核心挑战深度剖析
在深入框架设计之前,我们首先对 LLMs 应用于 Android 恶意软件检测时面临的核心挑战进行详细分析,为后续技术方案的设计提供依据。
1.1 上下文窗口限制问题
Android 应用通常包含数千个类文件以支持多样化的设备功能和业务场景,即使是经过编译后的 APK 文件,其反编译后的代码量也往往非常庞大。例如,一个普通的 Android 应用反编译后可能包含超过 150 万行代码,而目前即使是支持长上下文的 LLMs(如声称支持 200 万 token 的模型),在处理这类大规模代码时也会面临上下文窗口溢出的问题。
直接对代码进行截断处理虽然能让 LLMs 勉强处理,但会导致大量关键上下文信息丢失,使得恶意代码被淹没在海量良性代码中。如图 1 所示,某 LLM 在处理包含 154 万行代码的应用时,因超出上下文限制而无法完成分析,而我们的框架能够有效提取关键代码片段,让算力更弱的 LLM 也能完成准确检测。
1.2 程序结构复杂性难题
与自然语言不同,代码具有严格的语法结构和复杂的依赖关系,包括深度嵌套的控制流、复杂的 API 调用链、类继承体系等。Android 恶意软件还常常通过代码混淆、多组件交互、复杂函数调用等手段隐藏恶意意图,进一步增加了分析难度。
LLMs 的核心推理机制基于序列 token 建模,难以处理代码中的非序列结构依赖。例如,某恶意软件通过反射机制调用敏感 API,其恶意行为分散在多个函数和组件中,传统 LLM 的线性分析方式无法捕捉这些跨组件、跨函数的关联关系,导致误判为良性应用。
1.3 传统检测方法的局限性
除了 LLMs 面临的固有挑战,传统 Android 恶意软件检测方法也存在诸多问题:
- 概念漂移问题:恶意软件不断演化变异,传统基于标注数据集训练的模型难以适应新的恶意软件变体,检测性能会随着时间推移急剧下降。
- 数据集偏差:现有数据集往往存在时间戳模糊、样本选择随机等问题,导致训练出的模型在真实场景中泛化能力差。
- 缺乏可解释性:传统机器学习模型(如 SVM、随机森林)和深度学习模型(如 DNN)的检测结果往往是黑盒输出,无法提供人类可读的恶意行为分析报告,不利于安全分析师进行后续溯源和处置。
二、框架整体设计思路
针对上述挑战,我们设计了一套完整的上下文驱动检测框架,其核心思路是模拟安全分析师分析恶意软件的思维过程:首先识别可疑的 API 调用和关键代码片段,然后分析这些代码的上下文关联关系,最后综合判断应用的恶意性。框架整体架构如图 2 所示,主要包含两大核心组件:关键上下文提取和分层代码推理。
框架核心思路导图
框架的工作流程可概括为:
- 对输入的 APK 文件进行静态分析,提取可疑 API 及其相关的控制流和数据流信息;
- 通过反向程序切片算法,从海量代码中筛选出与恶意行为相关的关键代码片段;
- 构建结构化的控制流图(CFG)和函数调用图(FCG),保留代码的结构依赖关系;
- 采用三层推理机制,从函数级、API 级、APK 级逐步递进分析,生成检测结果和解释报告;
- 引入事实一致性验证机制,减少 LLM 幻觉带来的错误。
三、关键上下文提取技术细节
关键上下文提取是框架的基础,其目标是从完整的 Android 应用代码中筛选出与恶意行为高度相关的关键代码片段,去除冗余信息,为 LLM 分析减负。该模块主要包含可疑 API 收集和反向程序切片两大核心技术。
3.1 可疑 API 收集策略
恶意软件的核心恶意行为往往通过调用敏感 API 实现,如窃取用户隐私数据、发送恶意短信、未经授权的网络传输等。因此,识别可疑 API 是定位恶意行为的关键第一步。
我们将可疑 API 分为两大类,基于公开的 Android 安全知识库(包括权限模型、API 文档、恶意软件分析报告等)构建可疑 API 集合:
3.1.1 敏感数据访问 API
这类 API 主要用于获取用户敏感数据或系统关键信息,包括:
- 需要显式权限声明才能调用的 API,如访问通讯录(ContactsContract相关 API)、短信(Telephony.Sms相关 API)、位置信息(LocationManager相关 API)等;
- 无需权限声明但可直接访问敏感数据的 API,如ClipboardManager.getPrimaryClip()(获取剪贴板数据)、Build.SERIAL(获取设备序列号)等。
我们基于 PScout、SuSi 等 Android 权限分析工具,结合手动整理的恶意软件常用 API 列表,构建了包含 237 个敏感数据访问 API 的集合,覆盖了设备信息、用户隐私、系统资源等多个敏感维度。
3.1.2 敏感数据传输 API
这类 API 主要用于将敏感数据传输到外部服务器或其他应用,也称为 "sink API",包括:
- 网络传输 API:如HttpURLConnection、OkHttpClient、Socket等相关 API;
- 跨应用通信 API:如Intent传递数据、ContentProvider数据共享等;
- 存储写入 API:如写入外部存储、SD 卡等可能被用于留存敏感数据的 API。
通过对大量恶意软件样本的分析,我们整理了 189 个敏感数据传输 API,涵盖了常见的数据泄露通道。
3.1.3 API 收集实现代码
以下是可疑 API 收集的核心实现代码,基于静态分析工具对 APK 文件进行解析,提取所有 API 调用并与可疑 API 集合进行匹配:
import org.jf.dexlib2.iface.ClassDef;
import org.jf.dexlib2.iface.Method;
import org.jf.dexlib2.iface.MethodCall;
import org.jf.dexlib2.analysis.AnalyzedInstruction;
import org.jf.dexlib2.analysis.MethodAnalyzer;
import java.util.*;
import java.io.IOException;
public class SuspiciousApiCollector {
// 预定义的可疑API集合:敏感数据访问API
private static final Set<String> SENSITIVE_ACCESS_APIS = new HashSet<>(Arrays.asList(
"Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;",
"Landroid/telephony/TelephonyManager;->getSubscriberId()Ljava/lang/String;",
"Landroid/location/LocationManager;->getLastKnownLocation(Ljava/lang/String;)Landroid/location/Location;",
"Landroid/content/ClipboardManager;->getPrimaryClip()Landroid/content/ClipData;",
// 更多敏感访问API...
));
// 预定义的可疑API集合:敏感数据传输API
private static final Set<String> SENSITIVE_TRANSFER_APIS = new HashSet<>(Arrays.asList(
"Landroid/net/HttpURLConnection;->setRequestMethod(Ljava/lang/String;)V",
"Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;",
"Landroid/content/Intent;->putExtra(Ljava/lang/String;Ljava/io/Serializable;)Landroid/content/Intent;",
"Landroid/telephony/SmsManager;->sendTextMessage(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/app/PendingIntent;Landroid/app/PendingIntent;)V",
// 更多敏感传输API...
));
// 存储收集到的可疑API及其调用信息
private Map<String, List<ApiCallInfo>> suspiciousApiCalls = new HashMap<>();
/**
* 分析APK文件,收集可疑API调用
* @param apkPath APK文件路径
* @throws IOException 读取APK文件异常
*/
public void collect(String apkPath) throws IOException {
// 使用DexLib2解析APK文件
try (ApkFile apkFile = new ApkFile(new File(apkPath))) {
for (ClassDef classDef : apkFile.getClasses()) {
// 遍历所有类的方法
for (Method method : classDef.getMethods()) {
if (method.getCode() == null) continue;
// 分析方法指令
MethodAnalyzer methodAnalyzer = new MethodAnalyzer(
apkFile.getDexFile(), classDef, method, 0);
for (int i = 0; i < methodAnalyzer.getInstructionCount(); i++) {
AnalyzedInstruction instruction = methodAnalyzer.getInstruction(i);
// 检测API调用指令
if (instruction instanceof MethodCall) {
MethodCall methodCall = (MethodCall) instruction;
String apiSignature = methodCall.getMethodReference().getDefiningClass() +
"->" + methodCall.getMethodReference().getName() +
methodCall.getMethodReference().getProtobufSignature();
// 检查是否为可疑API
if (SENSITIVE_ACCESS_APIS.contains(apiSignature) ||
SENSITIVE_TRANSFER_APIS.contains(apiSignature)) {
// 记录API调用信息:类名、方法名、指令位置、API类型
ApiCallInfo callInfo = new ApiCallInfo(
classDef.getType(),
method.getName(),
i,
SENSITIVE_ACCESS_APIS.contains(apiSignature) ? "access" : "transfer"
);
// 添加到结果集合
suspiciousApiCalls.computeIfAbsent(apiSignature, k -> new ArrayList<>())
.add(callInfo);
}
}
}
}
}
}
}
/**
* 获取收集到的可疑API调用
* @return 可疑API调用映射表
*/
public Map<String, List<ApiCallInfo>> getSuspiciousApiCalls() {
return suspiciousApiCalls;
}
// 内部类:存储API调用信息
public static class ApiCallInfo {
private String className;
private String methodName;
private int instructionIndex;
private String apiType;
// 构造函数、getter和setter方法
public ApiCallInfo(String className, String methodName, int instructionIndex, String apiType) {
this.className = className;
this.methodName = methodName;
this.instructionIndex = instructionIndex;
this.apiType = apiType;
}
// getter方法
public String getClassName() { return className; }
public String getMethodName() { return methodName; }
public int getInstructionIndex() { return instructionIndex; }
public String getApiType() { return apiType; }
}
}3.2 反向程序切片算法
收集到可疑 API 后,我们需要提取与这些 API 调用相关的代码片段,去除无关代码,构建精简的控制流图。这里采用了改进的反向程序切片算法,通过追踪影响可疑 API 执行的变量和指令,实现关键代码的精准提取。
3.2.1 切片准则定义
程序切片的核心是定义合理的切片准则,我们将切片准则定义为C=<s, V>,其中:
- s:调用可疑 API 的语句(指令);
- V:该 API 调用所使用的所有参数变量集合。
切片的目标是提取程序中所有直接或间接影响s执行或V取值的指令集合,形成切片后的控制流图(Sliced CFG)。
3.2.2 相关变量分类
为确保切片的完整性和精准性,我们将相关变量分为两类:
- 直接相关变量:其值直接影响可疑 API 参数v∈V的变量;
- 间接相关变量:在分支语句中,其值影响可疑 API 是否被调用的变量。
例如,对于 API 调用sendTextMessage(phoneNum, null, content, null, null),phoneNum和content是直接相关变量;而控制该 API 是否执行的条件变量if (isMalicious) { sendTextMessage(...) }中的isMalicious则是间接相关变量。
3.2.3 改进的反向切片算法实现
传统反向切片算法在处理复杂分支结构和跨函数调用时容易出现遗漏,我们对其进行了优化,增加了跨函数递归切片和变量依赖追踪机制。算法的核心步骤分为变量检索和切片提取两个阶段:
import org.jf.dexlib2.iface.ClassDef;
import org.jf.dexlib2.iface.Method;
import org.jf.dexlib2.analysis.AnalyzedInstruction;
import org.jf.dexlib2.analysis.MethodAnalyzer;
import org.jf.dexlib2.iface.instruction.formats.Instruction35c;
import org.jf.dexlib2.iface.reference.FieldReference;
import org.jf.dexlib2.iface.reference.MethodReference;
import java.util.*;
public class BackwardSlicer {
// 存储切片结果:API签名 -> 切片后的控制流图
private Map<String, ControlFlowGraph> slicedCfgs = new HashMap<>();
private ApkFile apkFile;
public BackwardSlicer(ApkFile apkFile) {
this.apkFile = apkFile;
}
/**
* 执行反向程序切片
* @param suspiciousApiCalls 可疑API调用信息
*/
public void slice(Map<String, List<SuspiciousApiCollector.ApiCallInfo>> suspiciousApiCalls) {
for (Map.Entry<String, List<SuspiciousApiCollector.ApiCallInfo>> entry : suspiciousApiCalls.entrySet()) {
String apiSignature = entry.getKey();
List<SuspiciousApiCollector.ApiCallInfo> callInfos = entry.getValue();
for (SuspiciousApiCollector.ApiCallInfo callInfo : callInfos) {
try {
// 获取包含API调用的类和方法
ClassDef classDef = apkFile.findClass(callInfo.getClassName()).get();
Method method = findMethod(classDef, callInfo.getMethodName());
if (method == null) continue;
// 分析方法,构建完整控制流图
MethodAnalyzer methodAnalyzer = new MethodAnalyzer(
apkFile.getDexFile(), classDef, method, 0);
ControlFlowGraph fullCfg = buildControlFlowGraph(methodAnalyzer);
// 阶段1:变量检索 - 找出所有相关变量
Set<String> relevantVariables = retrieveRelevantVariables(
methodAnalyzer, callInfo.getInstructionIndex(), apiSignature);
// 阶段2:切片提取 - 提取相关指令
ControlFlowGraph slicedCfg = extractSlice(
fullCfg, methodAnalyzer, callInfo.getInstructionIndex(), relevantVariables);
// 处理跨函数依赖:递归切片调用者
handleInterproceduralDependencies(
classDef, method, slicedCfg, relevantVariables);
// 存储切片结果
slicedCfgs.computeIfAbsent(apiSignature, k -> new ControlFlowGraph())
.merge(slicedCfg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 阶段1:变量检索 - 找出所有影响API调用的相关变量
*/
private Set<String> retrieveRelevantVariables(
MethodAnalyzer methodAnalyzer, int instructionIndex, String apiSignature) {
Set<String> relevantVariables = new HashSet<>();
Queue<Integer> worklist = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
// 从API调用指令开始反向遍历
worklist.add(instructionIndex);
visited.add(instructionIndex);
while (!worklist.isEmpty()) {
int currIndex = worklist.poll();
AnalyzedInstruction instruction = methodAnalyzer.getInstruction(currIndex);
// 提取当前指令使用的变量
Set<String> variables = extractVariablesFromInstruction(instruction);
// 如果是API调用指令,提取其参数变量
if (isApiCallInstruction(instruction, apiSignature)) {
variables.addAll(extractApiParameters(instruction));
}
relevantVariables.addAll(variables);
// 获取前驱指令(反向遍历)
List<Integer> predecessors = getPredecessorInstructions(methodAnalyzer, currIndex);
for (int predIndex : predecessors) {
if (!visited.contains(predIndex)) {
visited.add(predIndex);
worklist.add(predIndex);
}
}
}
return relevantVariables;
}
/**
* 阶段2:切片提取 - 提取与相关变量相关的指令
*/
private ControlFlowGraph extractSlice(
ControlFlowGraph fullCfg, MethodAnalyzer methodAnalyzer,
int startIndex, Set<String> relevantVariables) {
ControlFlowGraph slicedCfg = new ControlFlowGraph();
Queue<Integer> worklist = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
worklist.add(startIndex);
visited.add(startIndex);
slicedCfg.addNode(startIndex, methodAnalyzer.getInstruction(startIndex));
while (!worklist.isEmpty()) {
int currIndex = worklist.poll();
AnalyzedInstruction currInst = methodAnalyzer.getInstruction(currIndex);
// 检查当前指令是否与相关变量相关
if (isRelevantToVariables(currInst, relevantVariables)) {
// 添加当前指令的前驱指令
List<Integer> predecessors = getPredecessorInstructions(methodAnalyzer, currIndex);
for (int predIndex : predecessors) {
if (!visited.contains(predIndex)) {
visited.add(predIndex);
worklist.add(predIndex);
slicedCfg.addNode(predIndex, methodAnalyzer.getInstruction(predIndex));
slicedCfg.addEdge(predIndex, currIndex);
} else if (!slicedCfg.hasEdge(predIndex, currIndex)) {
slicedCfg.addEdge(predIndex, currIndex);
}
}
}
}
return slicedCfg;
}
/**
* 处理跨函数依赖:递归切片调用者方法
*/
private void handleInterproceduralDependencies(
ClassDef currentClass, Method currentMethod,
ControlFlowGraph slicedCfg, Set<String> relevantVariables) {
// 分析当前方法中的函数调用,找出调用者
MethodAnalyzer methodAnalyzer = new MethodAnalyzer(
apkFile.getDexFile(), currentClass, currentMethod, 0);
for (int i = 0; i < methodAnalyzer.getInstructionCount(); i++) {
AnalyzedInstruction instruction = methodAnalyzer.getInstruction(i);
if (instruction instanceof MethodCall) {
MethodCall methodCall = (MethodCall) instruction;
MethodReference calledMethodRef = methodCall.getMethodReference();
// 查找被调用的方法
Optional<ClassDef> calledClassOpt = apkFile.findClass(calledMethodRef.getDefiningClass());
if (calledClassOpt.isPresent()) {
ClassDef calledClass = calledClassOpt.get();
Method calledMethod = findMethod(calledClass, calledMethodRef.getName());
if (calledMethod != null && calledMethod.getCode() != null) {
// 递归对被调用方法进行切片
MethodAnalyzer calledMethodAnalyzer = new MethodAnalyzer(
apkFile.getDexFile(), calledClass, calledMethod, 0);
ControlFlowGraph calledMethodCfg = buildControlFlowGraph(calledMethodAnalyzer);
// 提取与相关变量相关的切片
ControlFlowGraph calledSlicedCfg = extractSlice(
calledMethodCfg, calledMethodAnalyzer, 0, relevantVariables);
// 合并切片结果
slicedCfg.merge(calledSlicedCfg);
// 继续递归处理更深层次的调用
handleInterproceduralDependencies(
calledClass, calledMethod, slicedCfg, relevantVariables);
}
}
}
}
}
// 辅助方法:构建控制流图
private ControlFlowGraph buildControlFlowGraph(MethodAnalyzer methodAnalyzer) {
ControlFlowGraph cfg = new ControlFlowGraph();
for (int i = 0; i < methodAnalyzer.getInstructionCount(); i++) {
AnalyzedInstruction instruction = methodAnalyzer.getInstruction(i);
cfg.addNode(i, instruction);
// 添加控制流边
List<Integer> successors = getSuccessorInstructions(methodAnalyzer, i);
for (int succIndex : successors) {
cfg.addEdge(i, succIndex);
}
}
return cfg;
}
// 辅助方法:判断指令是否为目标API调用
private boolean isApiCallInstruction(AnalyzedInstruction instruction, String apiSignature) {
if (instruction instanceof MethodCall) {
MethodCall methodCall = (MethodCall) instruction;
String callSignature = methodCall.getMethodReference().getDefiningClass() +
"->" + methodCall.getMethodReference().getName() +
methodCall.getMethodReference().getProtobufSignature();
return callSignature.equals(apiSignature);
}
return false;
}
// 其他辅助方法:提取变量、获取前驱/后继指令等
// ...(省略具体实现)
/**
* 获取切片后的控制流图
*/
public Map<String, ControlFlowGraph> getSlicedCfgs() {
return slicedCfgs;
}
// 内部类:控制流图实现
public static class ControlFlowGraph {
private Map<Integer, AnalyzedInstruction> nodes = new HashMap<>();
private Set<Edge> edges = new HashSet<>();
public void addNode(int index, AnalyzedInstruction instruction) {
nodes.put(index, instruction);
}
public void addEdge(int from, int to) {
edges.add(new Edge(from, to));
}
public void merge(ControlFlowGraph other) {
this.nodes.putAll(other.nodes);
this.edges.addAll(other.edges);
}
public boolean hasEdge(int from, int to) {
return edges.contains(new Edge(from, to));
}
// getter方法和其他辅助方法
// ...
}
// 内部类:控制流边
public static class Edge {
private int from;
private int to;
public Edge(int from, int to) {
this.from = from;
this.to = to;
}
// equals和hashCode方法
// ...
}
}3.2.4 切片效果
通过反向程序切片算法,我们能够将原始的大规模代码精简为仅包含可疑 API 相关逻辑的代码片段。实验数据显示,切片后的代码量平均仅为原始代码的 5%-10%,但保留了所有与恶意行为相关的关键逻辑,使得 LLM 能够轻松处理,同时避免了关键信息丢失。
四、分层代码推理机制
为解决 LLM 难以处理复杂程序结构的问题,我们设计了三层代码推理机制,从细粒度到粗粒度逐步分析,既保证了分析的精准性,又能捕捉跨层级的依赖关系。同时,引入事实一致性验证机制,有效减少 LLM 的幻觉问题。
4.1 推理架构设计
分层代码推理机制分为三个层级,每个层级的输出作为下一层级的输入,形成递进式分析流程:
- 函数行为总结(L 1):对切片后的每个函数控制流图进行细粒度分析,总结函数的核心功能和行为模式;
- API 意图识别(L 2):基于函数调用图和函数行为总结,分析可疑 API 的整体意图,判断其是否被恶意使用;
- APK 恶意性判断(L 3):综合所有可疑 API 的意图分析结果,判断整个 APK 的恶意性,生成检测报告和恶意指标。
这种分层设计的优势在于:
- 有效控制每个层级的输入规模,避免超出 LLM 上下文限制;
- 逐步抽象代码语义,从指令级到函数级再到应用级,符合人类分析思维;
- 各层级各司其职,既保证了细节分析的精准性,又能捕捉整体恶意意图。
4.2 各层级详细实现
4.2.1 函数行为总结
该层级的核心目标是对切片后的函数控制流图进行细粒度分析,提取函数的关键行为特征,为后续 API 意图识别提供基础。
输入:切片后的函数控制流图(Sliced CFG);
输出:函数行为总结报告,包括函数功能描述、变量依赖关系、敏感操作等。
为了引导 LLM 进行精准分析,我们设计了专门的提示词模板:
def generate_l1_prompt(sliced_cfg, function_info):
prompt = f"""你是一名专注于Android恶意软件分析的网络安全专家,请详细分析提供的控制流图,该图包含与敏感API调用相关的指令。
函数信息:
- 类名:{function_info['class_name']}
- 方法名:{function_info['method_name']}
- 关联敏感API:{function_info['related_api']}
控制流图详情:
{nodes_str}
{edges_str}
分析要求:
1. 详细描述该函数的核心功能和执行流程;
2. 识别函数中使用的关键变量及其依赖关系;
3. 分析函数与敏感API的交互方式(参数传递、调用条件等);
4. 指出可能存在的恶意行为特征(如未授权数据访问、异常参数处理等);
5. 输出格式要求:分点列出分析结果,语言简洁明了,基于提供的代码事实,不添加无关推测。"""
return prompt其中,nodes_str和edges_str是将控制流图转换为 LLM 可理解的文本格式,包含每个节点的指令内容和节点间的控制流关系。
4.2.2 API 意图识别
同一 API 在不同上下文环境中可能具有不同的意图,例如getDeviceId()在日志统计场景中是良性的,但在数据外发场景中可能是恶意的。该层级通过分析与可疑 API 相关的所有函数调用关系,综合判断 API 的真实意图。
输入:函数调用图(FCG)、L 1 生成的函数行为总结、API 类型(访问 / 传输);
输出:API 意图总结报告,包括 API 的使用场景、数据流向、是否存在恶意使用等。
函数调用图(FCG)以可疑 API 为核心,构建所有相关函数的调用关系网络,每个节点代表一个函数,边代表函数间的调用关系,节点属性包含 L 1 生成的函数行为总结。
API 意图识别的提示词模板设计如下:
def generate_l2_prompt(api_name, api_type, fcg_info, function_summaries):
prompt = f"""你是一名专注于Android恶意软件分析的网络安全专家,请基于提供的函数调用图和函数行为总结,分析指定敏感API的主要功能和行为意图。
API信息:
- API名称:{api_name}
- API类型:{api_type}(access=敏感数据访问,transfer=敏感数据传输)
函数调用图详情:
{fcg_structure_str}
相关函数行为总结:
{function_summaries_str}
分析要求:
1. 综合所有相关函数的行为,分析该API的使用场景和目的;
2. 追踪与该API相关的数据流向(数据来源、处理过程、最终去向);
3. 判断该API是否被恶意使用(如未经授权访问数据、异常数据传输等);
4. 识别可能的规避手段(如反射调用、动态加载等);
5. 输出格式要求:先给出API意图结论,再分点列出支撑证据,基于提供的事实信息进行分析。"""
return prompt4.2.3 APK 恶意性判断
该层级是整个推理过程的最终环节,综合所有可疑 API 的意图分析结果,结合 Android 恶意软件的典型行为模式,判断整个 APK 的恶意性,并生成可解释的检测报告。
输入:所有可疑 API 的意图总结报告、APK 基本信息(权限申请、组件配置等);
输出:最终检测结果(恶意 / 良性)、关键发现、恶意指标(IoCs)、检测依据等。
APK 恶意性判断的提示词模板设计如下:
def generate_l3_prompt(api_intents, apk_basic_info):
prompt = f"""你是一名专注于Android恶意软件分析的网络安全专家,请根据提供的所有敏感API意图总结和应用基本信息,判断该Android应用是否为恶意软件,并给出详细的检测分析报告。
应用基本信息:
- 权限申请列表:{apk_basic_info['permissions']}
- 组件配置:{apk_basic_info['components']}
- 目标API级别:{apk_basic_info['target_sdk']}
敏感API意图总结:
{api_intents_str}
分析要求:
1. 综合所有敏感API的使用情况,判断应用整体是否为恶意软件(MALWARE)或良性应用(BENIGN);
2. 列出关键发现,包括敏感数据访问行为、数据传输行为、异常代码模式等;
3. 识别恶意指标(IoCs),如可疑API调用组合、异常权限使用、数据外发地址等;
4. 详细说明判断依据,每个结论需对应具体的API使用场景或代码行为;
5. 输出格式要求:先给出最终预测结果,再分关键发现、检测依据两部分展开,语言专业、逻辑清晰。"""
return prompt4.3 事实一致性验证机制
LLM 在生成总结和分析报告时存在幻觉风险,可能生成与原始代码不符的虚假信息,尤其是在处理复杂代码逻辑时。为解决这一问题,我们在 L 1 之后引入事实一致性验证机制,确保函数行为总结的准确性,避免错误信息在后续层级传播。
4.3.1 数据依赖定义
我们定义了五种核心数据依赖关系,覆盖代码中变量与 API、变量与变量之间的关键关联:
类别 | 类型 | 描述 | 示例 |
变量 - API 依赖 | 直接依赖 | 变量直接作为 API 参数影响其执行 | sendTextMessage(phoneNum) 中的 phoneNum |
变量 - API 依赖 | 传递依赖 | 变量通过赋值操作间接影响 API 执行 | r2 = getMaliciousPhone(); sendTextMessage(r2)中的 r2 |
变量 - API 依赖 | 条件依赖 | 变量通过控制流影响 API 是否执行 | if (isAttack) { sendTextMessage() } 中的 isAttack |
变量间依赖 | 并行依赖 | 两个变量共同参与计算影响 API 参数 | content = title + body; sendTextMessage(content) 中的 title 和 body |
变量间依赖 | 派生依赖 | 变量通过计算从其他变量派生而来 | secret = encrypt(data); sendData(secret) 中的 secret 和 data |
4.3.2 一致性验证实现
事实一致性验证的核心思路是:让 LLM 从切片后的控制流图中提取上述五种数据依赖关系,然后与人工提取的标准依赖关系进行比对,计算数据关系覆盖率(DRC),判断总结报告的准确性
public class FactualConsistencyVerifier {
// 一致性阈值,可根据实际场景调整
private static final double CONSISTENCY_THRESHOLD = 0.95;
private LLMClient llmClient;
public FactualConsistencyVerifier(LLMClient llmClient) {
this.llmClient = llmClient;
}
/**
* 验证函数行为总结的事实一致性
* @param slicedCfg 切片后的控制流图
* @param functionSummary LLM生成的函数行为总结
* @return 验证结果,包含DRC值和是否一致的判断
*/
public ConsistencyResult verify(BackwardSlicer.ControlFlowGraph slicedCfg, String functionSummary) {
// 步骤1:生成验证提示词,让LLM提取数据依赖关系
String prompt = generateVerificationPrompt(slicedCfg);
// 步骤2:调用LLM提取数据依赖
String llmExtractedDependencies = llmClient.generate(prompt);
List<Dependency> extractedDeps = parseDependencies(llmExtractedDependencies);
// 步骤3:人工提取的标准数据依赖(实际应用中可通过静态分析工具自动提取)
List<Dependency> groundTruthDeps = extractGroundTruthDependencies(slicedCfg);
// 步骤4:计算数据关系覆盖率(DRC)
double drc = calculateDRC(extractedDeps, groundTruthDeps);
// 步骤5:判断是否符合一致性要求
boolean isConsistent = drc >= CONSISTENCY_THRESHOLD;
// 步骤6:如果不一致,生成修正提示词
if (!isConsistent) {
String correctionPrompt = generateCorrectionPrompt(functionSummary, groundTruthDeps, extractedDeps);
String correctedSummary = llmClient.generate(correctionPrompt);
return new ConsistencyResult(drc, isConsistent, correctedSummary);
}
return new ConsistencyResult(drc, isConsistent, functionSummary);
}
/**
* 生成验证提示词
*/
private String generateVerificationPrompt(BackwardSlicer.ControlFlowGraph slicedCfg) {
// 将控制流图转换为文本格式
String cfgText = convertCfgToText(slicedCfg);
return f"""以下是一个函数的控制流图切片,包含调用敏感API的相关指令。请根据提供的控制流图,提取其中的变量依赖关系。
控制流图:
{cfgText}
依赖关系类型定义:
1. 直接依赖(Direct):变量直接作为API参数
2. 传递依赖(Transitive):变量通过赋值间接影响API执行
3. 条件依赖(Conditional):变量通过控制流影响API是否执行
4. 并行依赖(Parallel):两个变量共同参与计算
5. 派生依赖(Derived):变量通过计算从其他变量派生
输出要求:
严格按照以下模板输出,不要添加额外内容:
<依赖类型>: <变量名列表>
例如:
Direct: r2, r3
Transitive: r4
Conditional: isMalicious
Parallel: title, content
Derived: encryptedData, rawData""" ;
}
/**
* 计算数据关系覆盖率(DRC)
*/
private double calculateDRC(List<Dependency> extractedDeps, List<Dependency> groundTruthDeps) {
int correctCount = 0;
for (Dependency groundTruth : groundTruthDeps) {
for (Dependency extracted : extractedDeps) {
if (groundTruth.getType().equals(extracted.getType()) &&
groundTruth.getVariables().equals(extracted.getVariables())) {
correctCount++;
break;
}
}
}
return groundTruthDeps.isEmpty() ? 1.0 : (double) correctCount / groundTruthDeps.size();
}
// 其他辅助方法:解析依赖关系、提取标准依赖、生成修正提示词等
// ...
/**
* 验证结果封装类
*/
public static class ConsistencyResult {
private double drc;
private boolean isConsistent;
private String summary;
public ConsistencyResult(double drc, boolean isConsistent, String summary) {
this.drc = drc;
this.isConsistent = isConsistent;
this.summary = summary;
}
// getter方法
public double getDrc() { return drc; }
public boolean isConsistent() { return isConsistent; }
public String getSummary() { return summary; }
}
/**
* 依赖关系封装类
*/
public static class Dependency {
private String type;
private Set<String> variables;
public Dependency(String type, Set<String> variables) {
this.type = type;
this.variables = variables;
}
// getter、equals和hashCode方法
// ...
}
}4.3.3 验证效果
实验结果表明,事实一致性验证机制能够有效过滤 LLM 生成的错误信息,将函数行为总结的准确率从 82% 提升至 97% 以上。当 DRC 值低于阈值时,通过修正提示词让 LLM 重新生成总结,能够显著减少后续层级的推理错误。
五、 Todo优化点
- 优化反向切片算法,进一步提升关键代码提取的精准度和效率;
- 探索领域专用 LLM 微调,增强模型对 Android 恶意代码的理解能力;
- 结合动态分析技术,补充静态分析无法捕捉的 runtime 恶意行为;
- 优化框架部署方案,降低大规模应用的部署成本。
六、案例分析
为直观展示框架的检测效果,我们选取两个典型案例进行详细分析:
案例 1:上下文溢出场景
某 Android 应用反编译后包含 1,547,806 行代码,直接使用支持长上下文的 LLM 进行分析时,因超出 20MB 的上下文限制而失败。使用本文框架进行处理:
- 提取出 23 个可疑 API 调用,包括getDeviceId()、sendTextMessage()等;
- 通过反向切片算法,将代码精简至 86,420 行,仅保留与可疑 API 相关的代码;
- 经过三层推理,框架准确判断该应用为恶意软件,其核心恶意行为是窃取设备信息并发送恶意短信。
案例 2:恶意行为隐藏场景
某 SMSReg 家族恶意软件通过伪装成 xUtils 库来隐藏恶意行为,传统 LLM 误判为良性应用。本文框架的检测过程:
- 识别出getDeviceId()、getSubscriberId()、sendTextMessage()等敏感 API 的可疑调用;
- 通过函数调用图分析发现,这些 API 通过反射机制调用,且数据最终发送至境外服务器;
- 事实一致性验证确认分析结果准确,最终判断为恶意软件,并生成详细的恶意行为报告,包括数据窃取、不安全 SSL 处理、未经授权的位置跟踪等关键发现。
七、总结
本文提出的上下文驱动框架通过关键上下文提取和分层代码推理机制,成功解决了 LLMs 应用于 Android 恶意软件检测时面临的上下文限制和程序结构复杂性难题,实现了高效、可解释的恶意软件检测。核心创新点包括:
- 改进的反向程序切片算法,能够精准提取与可疑 API 相关的关键代码片段,大幅减少输入规模;
- 三层递进式代码推理机制,从函数级到应用级逐步分析,有效捕捉复杂程序结构依赖;
- 基于数据依赖的事实一致性验证机制,显著降低 LLM 幻觉带来的错误风险。
提出上下文提取与分层推理框架,突破 LLM 上下文限制与结构理解瓶颈,实现高效可解释检测。
由 AI 根据全文自动生成,仅供参考,以原文为准