共计 2692 个字符,预计需要花费 7 分钟才能阅读完成。
为什么需要分析函数调用图(FCG)
去年爆发的 FlyTrap 恶意软件家族,通过伪装成合法 VPN 应用窃取银行凭证。分析发现其核心恶意行为分散在 20 多个类中,只有通过函数调用关系才能发现:Runtime.exec()最终会触发隐蔽的短信转发操作。另一个案例是 Triada 木马,它通过动态加载 dex 文件来隐藏真实调用链,传统静态分析完全失效。

这些案例说明,单纯的字符串匹配或权限检测已经不够,必须通过 FCG(Function Call Graph)才能看清恶意代码的完整执行逻辑。
工具链选择:Soot vs Androguard
- Soot(Java 静态分析框架)
- 优势:支持 Jimple 中间语言,能处理复杂控制流;提供完整的 Call Graph API
-
劣势:需要手动处理多 DEX 和动态加载
-
Androguard(Python 工具包)
- 优势:内置 APK 解析器,开箱即用;适合快速原型开发
- 劣势:对混淆代码处理能力较弱
推荐组合方案:用 Androguard 做初步 APK 解包,再用 Soot 进行深度分析。
三阶段实战流程
阶段 1:用 Soot 生成控制流图(CFG)
// 初始化 Soot 环境
Options.v().set_src_prec(Options.src_prec_apk);
Options.v().set_process_dir(Collections.singletonList("sample.apk"));
Scene.v().loadNecessaryClasses();
// 选取目标类和方法
SootClass targetClass = Scene.v().getSootClass("com.malware.payload");
SootMethod targetMethod = targetClass.getMethodByName("triggerPayload");
// 生成基本块 (Basic Block) 级别的 CFG
UnitGraph cfg = new BriefUnitGraph(targetMethod.retrieveActiveBody());
// 可视化输出(需 Graphviz 支持)DotGraph dot = new DotGraph("malware_cfg");
for (Unit unit : cfg) {dot.drawNode(unit.toString());
for (Unit successor : cfg.getSuccsOf(unit)) {dot.drawEdge(unit.toString(), successor.toString());
}
}
dot.plot("cfg.png");
关键参数说明:
– set_src_prec 指定 APK 作为输入源
– BriefUnitGraph 生成精简版 CFG(忽略异常处理等边)
阶段 2:构建完整 FCG
跨组件调用处理流程:
[Activity] --Intent--> [BroadcastReceiver]
↓ ↓
[startService] [sendBroadcast]
↓ ↓
[Service.onStartCommand] [Receiver.onReceive]
关键代码片段:
// 使用 SparkCallGraphBuilder 构建全量调用图
CallGraph cg = new SparkCallGraphBuilder()
.setEntryPoint(method)
.build();
// 处理跨组件调用(需额外分析 AndroidManifest.xml)for (Edge edge : cg) {if (edge.src().getDeclaringClass().isAndroidClass()) {
// 标记系统 API 调用点
edge.tag("SystemCall");
}
}
阶段 3:图特征提取
使用 NetworkX 计算图指标:
import networkx as nx
def extract_features(cg):
G = nx.DiGraph()
# 添加节点和边
for node in cg.nodes():
G.add_node(node.hashCode(),
type=node.getMethod().getReturnType())
for edge in cg.edges():
G.add_edge(edge.src().hashCode(),
edge.tgt().hashCode(),
call_type=edge.kind())
# 计算关键特征
features = {'avg_degree': sum(dict(G.degree()).values()) / len(G),
'clustering': nx.average_clustering(G),
'assortativity': nx.degree_assortativity_coefficient(G)
}
return features
对抗混淆的解决方案
- 字符串加密
- 动态跟踪:Hook String.equals()方法
-
模式匹配:识别 Base64 特征 + 异或运算模式
-
反射调用
// 识别 Class.forName() + getMethod()模式 if (invokeExpr instanceof StaticInvokeExpr) {if (invokeExpr.getMethod().getName().contains("forName")) {logger.warn("发现反射调用点:" + stmt); } } -
动态加载
- 监控 DexClassLoader 的使用
- 提前解压 assets 中的可疑 dex/jar
性能优化技巧
-
增量分析:
只重新分析修改过的 APK 组件(通过 SHA256 对比) -
子图缓存:
def get_subgraph_cache(key): if redis.exists(key): return pickle.loads(redis.get(key)) else: g = build_subgraph(key) redis.setex(key, 3600, pickle.dumps(g)) return g
实践建议
- 数据集准备:
- MalwareBazaar 的 TOP20 家族样本(建议从 Emotet 开始)
-
VirusTotal 的 Android 恶意软件数据集(需申请 API 权限)
-
完整复现步骤:
- 使用 apktool 解包样本
- 用 Soot 生成初始 CFG
- 通过 Spark 构建全程序 FCG
-
提取图特征并训练简单分类器
-
进阶方向:
- 结合动态分析的 Hybrid CFG
- 使用图神经网络 (GNN) 进行家族分类
通过这个流程,即使是新手也能在 2 - 3 天内搭建出可用的恶意软件检测原型。关键是要理解:FCG 分析不是银弹,必须与其他技术(如权限分析、动态监控)结合使用。
正文完
