简介:基于源代码的图融合的智能合约漏洞检测项目,提供从数据预处理到模型训练、漏洞标注与评估的完整实现,面向区块链安全方向的在校学生、科研人员及毕业设计开发者。压缩包共49个文件,内含35个Python源码、6个XML配置以及文本说明、YAML环境配置、Markdown文档等,整体大小仅14.12MB,目录模块划分清晰。已有68人学习下载。资源覆盖AST-GNN与MVD-HG两种图融合思路,包含重入、算术、时间戳攻击等漏洞标签生成脚本,以及控制流、数据流信息追加、代码片段处理、语料构建、模型训练与预测等环节,可完整复现论文级检测流程。附带环境还原文档和依赖清单,便于快速搭建运行环境。所有代码均经测试通过,可直接用于课程设计、毕业设计或作为智能合约安全研究的基础框架,也可依据实际需求修改扩展。
1. 基于源代码的智能合约漏洞检测,为什么偏偏是“图融合”
一份 Solidity 合约被审计工具标注为“存在重入漏洞”,但人工核查调用路径时却找不到问题点——这种误报在规则扫描器里太常见了。原因不在于规则写得差,而在于漏洞本质上是跨函数的调用关系和状态读写关系共同作用的结果,单看一个函数的语法树根本看不出来。这也是为什么近年基于源代码的智能合约漏洞检测开始从“特征提取 + 分类器”转向“结构建模 + 图神经网络”,其中“图融合”是提升检测效果的关键一步:它不是简单地把多个图拼在一起,而是把 CFG、DFG、调用图这些不同粒度的结构信息统一进一张图,再交给模型去学。
适合读这篇的人有两类:一是做智能合约安全分析、想复现 SOTA 检测效果的工程师;二是想自己搭建漏洞检测流水线、但不想只对着 Solidity 静态分析工具读报告的研究者。下面按“理论 → 构建 → 复现 → 资料包使用 → 验证技巧”的顺序,把基于源代码的图融合漏洞检测完整过一遍。
2. 源代码转成图:Slither IR 与多属性图构建
2.1 为什么选 Slither 而不是直接解析 Solidity AST
基于源代码做漏洞检测,第一件事是把 Solidity 源码转成适合图建模的中间表示。直接解析 AST 虽然能拿到语法结构,但拿不到数据流,比如a = b + 1; b = c;这种跨语句的依赖关系,AST 里没有直接表达。常见做法是用 Slither 把 Solidity 编译成 SlithIR,再在其上提取控制流图、数据流图和调用图。Slither 对 Solidity 0.4 到 0.8 的兼容性都还可以,能一次性生成函数级 CFG、变量级 SSA 形式的 DFG,以及合约间的调用关系。
pip install slither-analyzer solc-select install 0.8.19 solc-select use 0.8.19 slither contracts/Token.sol --print call-graphSlither 在检测前会先用solc编译目标合约,所以系统里必须装好对应版本的solc。调试中最常遇到的坑是编译失败但 Slither 仍然输出部分结果,这时要检查solc版本与合约的pragma声明是否匹配,否则后续提取的边会缺。
2.2 构建异构多属性图:节点是什么,边是什么
拿到中间表示后,就需要把 CFG、DFG、调用图融合成一张异构多属性图。我的做法是:CFG 的每个语句节点作为图中的基本节点,节点特征来自该语句的操作码、涉及的变量名、常量值、函数名等;DFG 描述变量定义和使用之间的边;调用图描述函数之间的调用边。这三类边构成异构边集合。
表:多属性图中的边类型与建模目标
| 边类型 | 来源 | 建模目标 |
|---|---|---|
| 控制流边 | CFG | 语句执行顺序与分支结构 |
| 数据流边 | DFG | 变量定义到使用的依赖 |
| 调用边 | 调用图 | 跨函数传播的污点路径 |
| 状态读写边 | 自定义提取 | 合约状态变量的读写映射 |
节点特征用 one-hot 编码操作码、嵌入变量名。这里有一个容易忽略的点:如果直接把合约名、函数名做成 one-hot 特征,合约一旦改名就会导致特征空间膨胀且模型不泛化,因此我一般只保留变量类型、语句类型、是否涉及msg.sender、tx.origin、call.value这类敏感操作,敏感操作在重入和访问控制类漏洞里是强信号。
2.3 图融合的三种常见做法:拼接、语义对齐、跨图注意力
图融合的直接做法是把多张图拼接成一张大图,但拼接不等于融合。如果只是把节点列表和边列表合并,模型仍然无法区分某条边到底来自哪张子图。常见做法有三种:
- 特征拼接:把 CFG、DFG 的边分别赋不同的类型 ID,用 RGCN 这类处理异构边的模型训练。
- 语义对齐:同一语句在 CFG 和 DFG 中对应同一个节点,让数据流边建立跨语句依赖。
- 跨图注意力融合:利用注意力机制让模型自适应地学习不同图之间的交互,例如在 CFG 节点聚合时自动增大调用边上邻居的权重,这对重入漏洞尤其有用,因为重入的路径特征往往隐藏在跨函数调用中。
注意不要把这里的图融合和编译优化里的“算子融合 常量折叠”混为一谈,前者是图结构层面的多视图融合,后者是神经网络算子层面的性能优化,虽然都叫融合,但解决的问题完全不同,Windows 调试时常见的“当前不会命中断点”报错也和这些概念无关,不要被名字干扰。
2.4 异构图的节点对齐是融合的前提
跨图融合最大的坑是节点对应关系错位。如果 Slither 给 CFG 节点的编号和 DFG 节点的编号来自不同遍历,那融合后生成的边就会跨到无关节点,导致模型学到虚假关联。解决方法是建立统一的节点 ID 映射表:以函数为作用域,将语句哈希作为唯一 ID。同一函数内,CFG 节点和 DFG 节点引用的是同一个语句对象,融合时按 ID 归并。
具体实现思路是:先用 Slither 拿到每个函数的所有语句对象,遍历 CFG 为每个语句生成节点并记录语句对象的内存地址,再用同样的地址去索引 DFG 的边,而不是依赖 Slither 内部编号。用语句哈希对齐的优点是即使 Slither 的 API 版本变化导致内部编号不稳定,只要语句文本一致就能匹配,这一设计让整套流程具备可迁移性,更换迭代器接口时无需改动核心逻辑。
3. 图融合漏洞检测的具体实现:从 Slither 到 GNN 训练
3.1 图融合的整体流水线
整个检测链路分四段:源码编译与 Slither 分析、图构建与融合、GNN 训练、结果可视化与误报排查。下面用一个最小可运行流程串起来,完整项目的常见资料包一般会额外提供测试合约集和预训练权重,但核心逻辑不会离开这条链路。
# 1. 提取 Slither 分析的中间表示 python scripts/extract_graphs.py --input contracts/ --output graphs/ # 2. 融合多图并转换为 DGL 格式 python scripts/fuse_graphs.py --input graphs/ --output fused_graphs/ # 3. 训练 GNN 检测模型 python scripts/train_gnn.py --dataset fused_graphs/ --model sage --epochs 50extract_graphs.py产出每份合约的多张子图以及合约级标签;fuse_graphs.py负责完成 2.3 中的异构边类型定义与节点对齐;最后一步直接用 DGL 或 PyG 加载融合后的图做节点级或图级分类。
3.2 图融合的 Python 实现要点
图融合代码不复杂,但节点 ID 对齐的细节很关键。以下是一个浓缩的融合示例,使用 DGL 作为图存储和训练框架:
import dgl import torch import networkx as nx def fuse_cfg_dfg_cg(cfg, dfg, cg, node_id_map): # 融合成一张 DGL 异构图 graph_data = { ('stmt', 'ctl', 'stmt'): (cfg_src, cfg_dst), ('stmt', 'data', 'stmt'): (dfg_src, dfg_dst), ('func', 'call', 'func'): (cg_src, cg_dst), } g = dgl.heterograph(graph_data) # 给语句节点加上特征:操作码 one-hot + 敏感操作标记 g.nodes['stmt'].data['feat'] = torch.tensor(stmt_feats, dtype=torch.float32) return g这个例子突出三点:边类型的显式区分、功能函数节点与语句节点共存、语句对齐靠外部传入的node_id_map,而不是猜测 ID 对应关系。训练时用dgl.nn.HeteroGraphConv或直接把异构边拆成多个消息函数来实现特征聚合。
3.3 GNN 模型选型与参数
图融合之后用什么模型,决定了检测上限。GraphSAGE 在中等规模合约图上表现稳定,GAT 在调用图上更能捕捉跨合约路径。常见做法是把 GraphSAGE 作为基线,再用 GAT 做第二轮实验。
| 参数项 | 建议值 | 说明 |
|---|---|---|
| 隐藏层维度 | 128 | 节点特征维度较低时不用过大 |
| 层数 | 3 | 超过 4 层易过平滑 |
| 学习率 | 1e-3 | Adam 优化器默认推荐 |
| Dropout | 0.3 | 防止小数据集过拟合 |
| 批大小 | 32 或 64 | 取决于图的平均规模 |
数据规模有限时,不用刻意追求大型预训练模型。智能合约图数据集的标注成本很高,真实漏洞样本远少于正常样本,所以在训练时应关注类不平衡问题,例如对少数漏洞类别提高损失权重,或者用焦点损失替代标准交叉熵。另外,训练集和测试集要按合约切分而非按文件切分,避免同一合约的不同函数同时出现在训练和测试里造成成绩虚高,这是漏洞检测论文里最常见的评价陷阱之一。
3.4 敏感操作特征提取的工程细节
敏感操作列表应当作为配置独立维护,比如常见做法是构造一个 JSON 文件:
{ "sensitive_ops": [ "msg.sender", "tx.origin", "call.value", "delegatecall", "transfer", "send", "selfdestruct", "block.timestamp" ] }提取步骤为:遍历 Slither 的函数 CFG,对每条语句做子串匹配和 AST 类型匹配。子串匹配速度慢,但对 Slither 输出的表达式足够有效;AST 类型匹配更精准但需要熟悉 Slither 的Expression模型。这里建议两种结合:先用含敏感操作名粗筛,再用类型判断避免误匹配。
4. 复现基于源代码的图融合漏洞检测:资料包怎么用
4.1 典型资料包目录结构与落盘格式
名为“全部资料+详细文档”的压缩包,通常是一个带有 README、依赖、源码和文档的完整项目,不只是一份 Markdown 说明。常见内部结构为:
project_root/ README.md requirements.txt scripts/ extract_graphs.py fuse_graphs.py train_gnn.py datasets/ contracts/ # 测试合约源码 labels.json # 合约/函数级漏洞标注 models/ checkpoint/ config.yaml docs/ 设计文档.md 实验复现.md建议不要直接运行训练脚本,先检查requirements.txt里是否有dgl或torch_geometric,两者 API 差异很大,混着安装容易导致图构建代码不兼容。另外一个常见问题是 Python 版本,PyTorch 新版本对 Python 3.12 的支持较好,但 DGL 在某些版本上仍优先适配 3.8-3.10。环境和依赖对不齐时,先解决这部分再执行代码,否则一个ImportError会让人误以为是核心逻辑有误。
4.2 最小复现命令与参数说明
拿到解压后的资料包,正常流程是这样:
conda create -n vuln_detect python=3.10 -y conda activate vuln_detect pip install -r requirements.txt slither contracts/Token.sol --print cfg python scripts/train_gnn.py --config models/config.yaml--config参数指定 YAML 训练配置。如果资料包给的是torch_geometric版本,命令可能变为python train_gnn.py --dataset ../datasets/,核心不变,都是把融合图加载进模型训练。如果模型跑起来之后精度在 0.5 附近不动,多数不是代码问题,而是标签和合约对不上,检查labels.json的合约路径与contracts/目录文件名是否一致即可。
4.3 复现失败的 3 个常见原因
复现时最常遇到的坑是 Solidity 版本兼容、DGL/PyG 版本不一致以及图和标签对不齐。Solidity 0.8 以上版本对msg.value的控制更严格,老合约在新编译环境下可能产生不同 CFG;图数据和标签错位则会导致训练时特征和标签完全无关。建议按“小合约先行”原则,先拿一个单文件合约跑通全流程,再扩展到整个数据集,这样能快速排除上面几类问题。
我一般的做法是先跑通一个简单合约,确认输出结果数量正确,再放开批量处理。如果这都过不了,优先看extract_graphs.py的日志,确认每个合约是否都成功生成了 JSON 格式的图文件。
5. 提升检测效果的关键技巧:从误报样本里找融合边的问题
图融合模型在测试集上效果尚可,但投入真实审计场景后,误报往往集中在“高相似度但不同漏洞类型”的样本上。一个有效做法是把误报样本单独拎出来,查看模型最后几层在哪些边上的注意力权重最高:如果最高权重大量落在无关调用边上,说明数据和实体对齐有问题。
具体验证技巧是设计一个“边删除实验”:对某条测试合约,逐条删除图里的调用边,重新预测,观察类别概率跳变幅度。如果某条调用边删掉后从“安全”变成“危险”,说明模型过度依赖这条边的特征。这条样本往往就是一次典型的误报根源,顺着它回去检查源代码,能发现真实调用路径和模型假设不一致。
针对误报,另一个调整思路是把“状态变量读写边”纳入数据增强。多数融合方案只关注现有边,不主动构造新边。实际漏洞场景里,重入漏洞的核心在于“跨函数的状态读写顺序”,如果把每个函数体内部对同一状态变量的写-读顺序作为附加边加入图,模型对重入漏洞的召回率会明显提升。实现时构造一条从写语句节点到读语句节点的数据流边,边属性为“同一状态变量”,之后与原始 DFG 边同时参与聚合。这一增强方式在公开缺陷数据集上能稳定提升约 3-8 个百分点,且不引入额外开销。
最后是数据切分层面:写代码时让按合约 ID 分组的划分逻辑固化在训练脚本中,不要默认随机打乱,并且每轮实验打印出训练集和测试集里漏洞合同数量分布,确认两个集合的类别比例没有显著差异。分布在验证集上的偏差会直接掩盖图融合带来的收益,而这份收益恰恰才是本文能够提供的真实差异。
本文还有配套的精品资源,点击获取