ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

图融合+GNN:智能合约漏洞检测的完整实践

2026/9/11 23:06:08 拓冰建站 浏览量
图融合+GNN:智能合约漏洞检测的完整实践 简介这是一份基于源代码的图融合的智能合约漏洞检测毕业设计项目资源使用Python实现面向计算机、人工智能、自动化等专业学生、老师及从业者适用于课程设计、大作业或毕业设计也便于入门者进阶。项目完整实现了数据清洗、语料构建、图结构转换、漏洞标签生成、模型训练与评估等流程标签生成环节覆盖重入、算术溢出、时间戳依赖等典型漏洞场景。资源共48个文件以35个Python脚本为主另有docx使用手册、txt环境说明、yml依赖及工程配置整包约14.84MB目录结构清晰目前已有111人学习下载。项目答辩获98分代码经过调试可运行并附有详细的环境还原过程等实测笔记借助完整源码与使用文档可快速复现实验学习图融合算法在智能合约漏洞检测中的应用并可在基础上二次开发扩展更多漏洞检测类型。1. 基于源代码的图融合把智能合约漏洞检测变成图上的分类任务传统智能合约漏洞检测基本靠静态规则匹配到.call.value就报重入看到block.timestamp就标时间戳依赖。规则写多了就发现两个问题冷门漏洞难写规则同一规则换一个合约误报率差好几倍。基于源代码的图融合是另一条路先把 Solidity 源码解析成抽象语法树再把控制流、数据流作为额外边叠加到树上交给图神经网络自己学“漏洞子图长什么样”。这个毕业设计项目把重入、时间戳、算术三类漏洞的检测做成了完整链路源码清洗、漏洞标注、图信息附加、模型训练、加载预测全套都有。项目里分了两套分类任务contract_classification 判断整个合约有没有问题line_classification 定位到具体代码行。对需要快速复现图神经网络检测流程的研究生来说这套代码比纯理论论文直观得多对想拿毕设框架改功能的人来说脚本拆得很细替换数据集和标签生成逻辑就能扩展成新题目。里面还有完整的文档和环境还原过程说明照着跑通整条链路比单纯调包理解深得多。2. 图融合的核心实现AST、CFG、DFG 三类边如何叠加到一张图上2.1 为什么单独一棵 AST 学不到漏洞AST 把合约源码组织成树函数声明、表达式、语句按语法层级挂在树节点下。但树边只表达“属于”关系不表达执行顺序也不表达数据流动。以重入漏洞为例真正危险的是外部调用之后仍然改状态形成“调用→状态写”的先后关系。这种关系在 AST 里体现为某个调用语句的兄弟节点顺序模型如果只沿树边做消息传递很难把远距离的两个节点关联起来。CFG 解决顺序问题。编译器解析出一个函数后按实际执行路径把每个语句连起来循环、分支、跳转都显式化。DFG 解决依赖问题变量在哪里被赋值、在哪里被使用用一条边直接连接。图融合的做法是把 CFG 边和 DFG 边按节点 id 对应到 AST 树上三种边共存于一张图。GNN 在一个节点的邻域里做聚合时同时看到语法结构、执行顺序和数据依赖漏洞特征自然比单棵树学得准。2.2 控制流和数据流是怎么构造并附加到 AST 上的项目里 append_control_flow_information.py 和 append_data_flow_information.py 就是干这件事的。执行顺序通常是先用编译器或解析工具拿到合约 AST节点带全局唯一 id再遍历函数体把函数内部语句按执行顺序生成控制流边最后扫变量读写把赋值点和使用点连成数据流边。这里最容易出问题的是 id 对齐CFG 和 DFG 的节点并不总是 AST 叶子节点可能是语句或表达式节点如果直接用编译器返回的原始 id经常出现连到不存在的节点。append_method_message_by_dict.py 处理的就是方法级信息对齐。它先把每个函数的方法名、参数、返回类型、起始和结束行列号存进字典后续构边时按行号范围定位 AST 节点。这样可以避免遍历整棵树反复查找父节点函数边界不清晰时也能兜底。# 融合逻辑的核心步骤把 CFG 和 DFG 边挂到 AST 节点上 def fuse_flow_edges(ast_nodes, cfg_edges, dfg_edges): # ast_nodes: {node_id: {type: ..., children: [...]}} # cfg_edges: [(src_node_id, dst_node_id)] # dfg_edges: [(src_node_id, dst_node_id)] for node_id, node in ast_nodes.items(): node.setdefault(edges, []) # 先挂控制流边再挂数据流边 for src, dst in cfg_edges: ast_nodes[src][edges].append({dst: dst, type: CFG}) for src, dst in dfg_edges: ast_nodes.setdefault(src, {type: expr}).setdefault(edges, []) ast_nodes[src][edges].append({dst: dst, type: DFG}) return ast_nodes这段代码是简化示意但能表现出融合的核心循环。cfg_edges里的 src 和 dst 必须是 AST 节点 idedge结构里同时存目标节点和边类型后面模型构造邻接矩阵时按类型拆成两个矩阵即可。我在实际做类似项目时会额外检查一遍边的两端是否存在缺失的就丢弃不然训练时 GNN 的索引会越界。2.3 融合边和三类漏洞的对应关系边类型承载的信息主要对应的漏洞AST 边语法嵌套层次所有漏洞提供基础结构CFG 边语句执行顺序重入、先调用后写状态DFG 边变量定义到使用时间戳依赖、算术问题具体来说重入漏洞的典型子图是“call 节点经由 CFG 边连到状态变量写节点”。模型学到这个子图特征后即使代码换成addr.call{value: x}()这种新式写法只要 AST 结构一致照样能识别。时间戳依赖的典型路径是“block.timestamp 节点经由 DFG 边连到 if 或 require 节点”。算术漏洞则关注减法、乘法节点的操作数来源如果操作数节点有外部输入的可达 DFG 路径就值得标记为可疑。2.4 边类型不是越多越好一个常见误区是认为融合的图信息越多模型越准。实际做下来跨函数调用边不加限制会把图撑得很大训练显存和收敛速度都受影响。我一般会保留函数内部的全量 CFG 和 DFG跨函数调用只保留调用点和被调函数入口之间的一条边函数与函数之间不额外建边。这样图规模可控GNN 又能在函数边界处做必要的上下文传播。这份资源里把控制流和数据流拆成两个独立脚本处理目的就是让你能分别验证两种边的贡献而不是直接堆积信息。提示融合边如果超过 AST 边数量的两到三倍先别急着调模型回到构边阶段检查有没有重复边或自环这类噪声对训练干扰很大。3. Solidity 源码到训练样本标签生成、代码清洗与向量化流水线3.1 三类漏洞的标签生成脚本写的是什么项目里 make_reentry_attack_label.py、make_timestamp_attack_label.py、make_arithmetic_attack_label.py 三个脚本分别生成重入、时间戳、算术三类漏洞的监督标签。标签不依赖人工打标而是按漏洞模式写启发式规则批量扫描。重入标签的判断核心是顺序找到外部调用点之后再检查紧随其后的代码块有没有状态变量赋值。时间戳标签则先找所有block.timestamp或now的使用节点再看这些节点的数据流是否流入 if 条件、require 判断、随机数生成或出参计算。算术标签更宽重点找外部输入参与且没有溢出防护的加减乘除。# make_reentry_attack_label.py 的简化逻辑 def label_reentrancy(func_ast): for call in find_external_calls(func_ast): # 找到调用点之后按源码顺序继续走 for stmt in call.get_following_statements(): if is_state_write(stmt): return 1 # 调用后写状态疑似重入 return 0get_following_statements因为是 AST 顺序遍历返回调用点之后的兄弟语句is_state_write判断语句是否对合约状态变量赋值。这个启发式考虑的是最典型的重入模式。实际项目中我还会加一个例外判断如果前置就先把防重入锁变量置为 true调用后再写锁变量这种写法不标记否则会把正确的防护代码判成漏洞。3.2 预处理流水线的执行顺序和各脚本作用数据准备脚本很多执行顺序很关键。路径是原始 Solidity 源码 → remove_blank.py 去空行 → remove_comments.py 去注释 → classification_of_documents.py 文档归类 → create_code_snippet.py 切成代码片段 → hash_code.py 去重 → built_corpus.py 构建词表 → built_vector_dataset.py 生成向量数据集。remove_comments 这一步不能省。Solidity 注释里经常出现block.timestamp、call.value这类提示词模型会把注释当成漏洞特征在测试集上被带偏。create_code_snippet 建议按函数边界切片一个片段对应一个函数再附上若干行上下文太长会稀释局部漏洞特征太短则丢失调用关系。hash_code 用哈希去重移除大量重复合约训练集更干净训练时间也能缩短。3.3 一条训练样本的格式与存储项目中 dataset.py 和 bean/Node.py 负责样本对象的组织。训练样本应该同时保存代码文本、图边列表和标签。格式上我习惯让每个函数样本存成 JSON长这样{ contract_id: sample_0001, func_name: withdraw, code_lines: [ function withdraw(uint amount) public {, require(balances[msg.sender] amount);, msg.sender.call.value(amount)();, balances[msg.sender] - amount;, } ], edge_list: [ [3, 4, CFG], [2, 4, DFG] ], label: 1 }edge_list里的三元组分别是起点行号、终点行号和边类型模型端构造邻接矩阵时按行号映射到 embedding 后的节点位置。label是合约级或片段级标签。如果做行级分类label 应该改成和 code_lines 等长的数组逐行标 0 或 1比如[0,0,1,1,0]这样模型才能在推理时输出“第几行可疑”。注意边列表尽量用字符串行号而不是直接存图节点 id因为预处理阶段反复修改代码片段行号比节点 id 稳定对齐成本低得多。4. AST-GNN 双路径训练合约级分类和行级定位的模型落地4.1 模型怎么读融合图model.py 和两个 model 文件定义的网络结构核心是图神经网络编码器。输入是节点特征矩阵和边索引节点特征由代码 token 嵌入和 AST 节点类型 embedding 拼接而成。CFG 边和 DFG 边不合并成同一个邻接矩阵而是在消息传递时分开处理让模型知道一条边是控制依赖还是数据依赖。contract_classification_model.py 输出的是整个合约或代码片段的表示经过全连接层后做二分类。line_classification_model.py 则对每个节点预测一个漏洞概率训练时用逐行标签计算交叉熵。两套模型共用同一个图编码器实际写代码时可以继承同一个基类只替换最后的池化层和分类头。# 边类型感知的图卷积示意 def edge_aware_conv(x, adj_cfg, adj_dfg, w_cfg, w_dfg): # x: [num_nodes, hidden_dim] # adj_cfg, adj_dfg: [num_nodes, num_nodes] h adj_cfg x w_cfg adj_dfg x w_dfg return torch.relu(h)这个实现把两种邻接矩阵分别乘上各自权重再相加。看起来简单但很实用两个权重矩阵让模型自己学习 CFG 和 DFG 边的不同贡献。如果数据量够大可以把相加改成注意力加权每个节点动态决定两种边的权重。层数一般控制在两层到三层程序图的直径小层数过多会把所有节点特征变成一样的分布也就是图神经网络常见的过平滑。4.2 训练命令和参数对照环境依赖在 requirements.txt 和 environment.yml 里项目里还附了 MVD-HG 环境的还原过程文档。我建议独立建一个 conda 环境避免和本机其他项目产生依赖冲突。按脚本执行依赖顺序一般是# 1. 数据清洗与向量化 python data_process.py python remove_comments.py python create_code_snippet.py python built_corpus.py python built_vector_dataset.py # 2. 生成三类漏洞标签 python make_reentry_attack_label.py python make_timestamp_attack_label.py python make_arithmetic_attack_label.py # 3. 训练和验证 python contract_classification_train.py python line_classification_train.py这份资源里还有 main.py 和 train.pymain.py 更像把预处理、标注、训练的编排动作合并成一个入口train.py 则单独承担模型训练主流程。如果只想快速跑通演示直接执行 main.py 也可以但建议先手动分步跑一遍能定位是哪一步出错。下面给一套常用训练参数来自我复现类似 GNN 检测任务的经验参数参考值说明hidden_dim128图节点隐层维度num_layers2图卷积层数超过 4 层容易过平滑learning_rate5e-4Adam 默认范围dropout0.5对合约数据泛化更稳class_weight[1.0, 3.0]正样本少时提高权重batch_size32程序图无 paddingbatch 大容易爆显存epochs80配合早停看验证集 F14.3 推理时最容易踩的坑项目里有 Test.py、Test2.py 和 load_model_to_predict.py前者跑验证集后者加载训练好的模型预测新代码。加载模型做推理时最容易出错的是特征不对齐训练时 built_corpus.py 建了词表预测新代码必须用同一份词表做 token 映射否则新 token 全被映射成 UNK预测结果接近随机。# 用训练好的模型预测新合约每一行的风险 from load_model_to_predict import load_model, predict_lines model, vocab load_model(checkpoints/line_classification.pt) new_code [ function withdraw(uint amount) public {, if (block.timestamp deadline) {, msg.sender.call.value(amount)();, }, } ] preds predict_lines(model, vocab, new_code, top_k1) print(preds) # 示例输出: [(2, 0.93)]predict_lines内部会做切片、token 映射和图构造示例输出表示第 2 行索引从 0 开始有 93% 概率是可疑行。实际使用中我会把阈值从默认 0.5 往下调行级定位更看重召回率多标出一行让审计人员复核的成本远低于漏掉真正漏洞的代价。5. 扩展新漏洞类型与提升行级定位精度的实用技巧5.1 给项目加一种新漏洞类型怎么改想扩展第四种漏洞比如拒绝服务先写一个 make_dos_attack_label.py 模仿现有标签脚本定义 DoS 的启发式规则再改 config.py 里的类别定义从三类扩为四类。同一份代码如果可能同时存在多个漏洞标签要改成多标签模型输出层用 sigmoid 加 BCE 损失而不是 softmax 交叉熵。改完之后重新跑一遍数据流程词表和向量数据集才能同步。我自己做扩展时更推荐先别合并模型。新漏洞单独训练一个二分类模型验证集 F1 过了 0.8 再考虑统一否则误报来源很难分清是标签问题还是模型结构问题。5.2 行级定位精度优化技巧定位精度可以从三个方向优化。第一个是片段级预测把合约按函数切成片段片段内做逐行分类再把行号映射回原文件。第二个是阈值调优行级样本正负比经常到 1:20在验证集扫一遍阈值选择 F1 最高的点而不是默认 0.5。第三个是置信度检查模型打出 0.95 以上极高分的节点先确认是不是注释残留或字符串常量里带了关键词这类样本要尽早清理。最后建议做一次消融实验只保留 AST 边的模型作为 baseline然后逐一加 CFG 和 DFG观察每类漏洞 F1 的变化。重入和时间戳提升会比较明显如果算术漏洞不升反降重点检查数据流边构造时是不是把表达式的内部临时变量也连进去了。本文还有配套的精品资源点击获取