ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于图神经网络的切片级漏洞检测与解释模型实战

基于图神经网络的切片级漏洞检测与解释模型实战 简介本资源为Python基于图神经网络的切片级漏洞检测及解释方法完整项目源码面向计算机、信息安全、人工智能等专业学生及研究人员可用于毕业设计、课程设计、大作业或初期项目立项。项目覆盖从数据预处理到模型训练与解释的全流程代码标准化、Joern生成PDG图与JSON、代码切片、w2v嵌入、切片级漏洞检测模型训练以及基于改进GNNExplainer或PGExplainer的漏洞解释与效果评定并附RQ3实例的源代码、切片dot文件与解释行号。压缩包共217个文件以90个py脚本、89个pyc缓存、10个json配置、8个dot图文件为主另有txt、png、sh、md、pkl等辅助文件整体约1.97MB目录结构清晰便于按模块检索。目前已有166人学习下载适合希望深入图神经网络与漏洞检测方向的学习者参考借鉴也可在现有代码基础上修改扩展实现新功能。1. 从 xen 的 8 个 dot 文件说起这套切片级漏洞检测源码到底能跑出什么如果你手头正好有一份1_xen-4.12.0-4API#4.dot这样的文件又看到pointer#10.dot、API#7.dot混在一起大概率会先愣一下这些 dot 是干嘛的它们其实是 Joern 从 xen 源码里抽出来的程序依赖图PDG每个文件对应一个代码切片。这套 Python 源码要干的事就是把这些切片喂给图神经网络判断它有没有漏洞并且告诉你「是哪几行代码导致的」。它解决的不是「扫一遍关键字」这种粗活而是把漏洞检测落到切片粒度再配一个解释模型让结果不再是黑匣子。适合做毕设、课程设计、期末大作业也适合想入门图神经网络 漏洞检测这条线的同学。下面我按自己拆包复现的顺序把预处理、训练、解释、避坑一次讲透。2. 数据预处理流水线从 raw_data_preprocess 到切片生成2.1 先理清整条链路的输入输出这套源码的预处理不是一步到位而是四段接力读漏洞行信息 → 代码标准化 → Joern 生成 PDG 和 json → 切片。每一段的输出都是下一段的输入路径写错一步后面全崩。我一般会先在纸上画一遍数据流向再动手改路径。第一段preprocess/raw_data_preprocess.py读的是 csv 文件拿到漏洞减号行的相关信息。减号行格式参考nvd_vul_lineinfo.json样例里面记录了哪个文件的哪一行是漏洞修复时被删掉的行。这一步的输出是后续切片要用的「漏洞行字典」。第二段preprocess/code_normalize/normalization.py做代码标准化。为什么要标准化因为同一个漏洞在不同项目里变量名、空格、注释都不一样不统一格式Joern 解析出来的图结构会飘。常见做法是去掉多余空白、统一换行、剥离注释让代码结构干净。第三段preprocess/joern_graph_gen.py调 Joern 生成 pdg 图和 json 文件。这一步是整条链路里最容易翻车的地方因为它依赖 Joern 本体和 Java 环境。脚本里用-i和-o指定输入输出路径然后分三次执行先-t parse生成解析结果.bin文件再-t export -r pdg生成pdg.dot最后-t export -r lineinfo_json生成包含全部 cpg 信息的lineinfo.json。第四段preprocess/slice_preocess/main.py生成代码切片。输入是lineinfo.json、pdg.dot以及修改后的vul_lines字典路径输出是complete_pdg和slice_pdg两类。complete_pdg是完整图slice_pdg是围绕漏洞行切出来的子图后面训练用的是切片。2.2 代码标准化与 Joern 生成 PDG 的具体操作先跑标准化。这一步脚本里路径是硬编码的必须手动改# preprocess/code_normalize/normalization.py import os # 改成你自己的源码根目录 SOURCE_ROOT /data/xen-4.12.0-4 # 标准化后的输出目录 NORM_ROOT /data/xen_normalized def normalize_file(src_path, dst_path): with open(src_path, r, encodingutf-8, errorsignore) as f: lines f.readlines() cleaned [] for line in lines: # 去掉行尾空白统一缩进为空格 line line.rstrip() # 剥离单行注释避免注释干扰图结构 if // in line: line line[:line.index(//)] cleaned.append(line) with open(dst_path, w, encodingutf-8) as f: f.write(\n.join(cleaned)) if __name__ __main__: for root, _, files in os.walk(SOURCE_ROOT): for name in files: if name.endswith(.c) or name.endswith(.h): src os.path.join(root, name) rel os.path.relpath(src, SOURCE_ROOT) dst os.path.join(NORM_ROOT, rel) os.makedirs(os.path.dirname(dst), exist_okTrue) normalize_file(src, dst)逻辑说明遍历源码目录只处理.c和.h把注释和行尾空白去掉。参数上SOURCE_ROOT和NORM_ROOT必须改成你自己的路径别直接跑。注释剥离这里用的是简单规则如果代码里有字符串里带//的情况会误伤后面 Joern 解析可能报错遇到再单独处理。标准化完跑 Joern 生成图# preprocess/joern_graph_gen.py 内部调用的命令手动执行时等价于 # 第一步parse生成 .bin joern -i /data/xen_normalized -o /data/joern_out -t parse # 第二步export pdg生成 dot joern -i /data/joern_out -o /data/joern_out -t export -r pdg # 第三步export lineinfo_json生成 json joern -i /data/joern_out -o /data/joern_out -t export -r lineinfo_json参数说明-i是输入目录-o是输出目录-t指定任务类型。parse阶段会把源码转成 Joern 内部的 CPG 表示存成.binexport -r pdg导出程序依赖图export -r lineinfo_json导出带行号信息的 json。注意-i和-o在第二步之后最好指向同一个目录否则.bin找不到。Joern 版本不同命令参数可能有差异常见做法是先joern --version确认一下。2.3 切片生成与 w2v 嵌入切片这一步的输入有三个lineinfo.json、pdg.dot、vul_lines字典。vul_lines就是第一段raw_data_preprocess.py产出的漏洞行信息格式是{文件名: [行号列表]}。跑之前把路径改对# preprocess/slice_preocess/main.py import json # 改成你的实际路径 LINEINFO_JSON /data/joern_out/lineinfo.json PDG_DOT /data/joern_out/pdg.dot VUL_LINES_PATH /data/vul_lines.json OUT_DIR /data/slices with open(VUL_LINES_PATH) as f: vul_lines json.load(f) # 后续逻辑根据 vul_lines 里的行号在 pdg 中定位节点 # 向前向后各扩展若干跳切出 slice_pdg # complete_pdg 保留完整图slice_pdg 只保留漏洞相关子图逻辑说明切片的核心是「以漏洞行为中心在 PDG 上做邻域扩展」。扩展跳数一般设 2 到 3 跳太小会丢上下文太大会引入噪声。输出目录里complete_pdg和slice_pdg分开存训练时只读slice_pdg。切片有了接着训练 w2v 模型做节点嵌入# preprocess/train_w2v.py from gensim.models import Word2Vec # 输入是切片里所有节点的 token 序列 SENTENCES_PATH /data/slices/node_tokens.txt W2V_MODEL_PATH /data/w2v.model sentences [] with open(SENTENCES_PATH) as f: for line in f: sentences.append(line.strip().split()) # vector_size 一般设 64 或 128窗口 5min_count 设 1 model Word2Vec(sentences, vector_size128, window5, min_count1, workers4) model.save(W2V_MODEL_PATH)参数说明vector_size是嵌入维度128 是常见起点window是上下文窗口min_count1是因为漏洞切片里很多 token 只出现一次设大了会被过滤掉。训练完执行preprocess/joern_to_devign完成嵌入同样注意路径。这一步的输出就是带节点嵌入的切片后面检测模型直接读。3. 漏洞检测模型训练main.py 里的路径、划分与参数3.1 数据集划分与路径配置检测模型的入口是slice_level_model/main.py。跑之前要做三件事改输入输出路径、自己划分 train 和 test、确认模型参数。路径包括数据集路径和模型保存位置数据集就是上一步产出的带嵌入切片。划分 train 和 test 的方式是写两个 txt 文件每个文件里一行一个切片文件名。我一般按 8:2 分注意要随机打乱别按文件名排序直接切否则同一项目的切片会集中在一侧测试结果虚高。# 划分脚本自己写一个就行 import os import random SLICE_DIR /data/slices/slice_pdg train_txt /data/train.txt test_txt /data/test.txt files [f for f in os.listdir(SLICE_DIR) if f.endswith(.dot)] random.seed(42) random.shuffle(files) split int(len(files) * 0.8) with open(train_txt, w) as f: f.write(\n.join(files[:split])) with open(test_txt, w) as f: f.write(\n.join(files[split:]))逻辑说明random.seed(42)保证可复现split控制比例。写进 txt 的是文件名main.py里读这两个文件来加载数据。注意路径要和main.py里的配置一致别一个用绝对路径一个用相对路径。3.2 模型参数与训练过程main.py里的模型参数一般包括学习率、batch size、epoch 数、隐藏层维度。这套源码用的是图神经网络常见结构是 GCN 或 GAT 堆几层最后接一个分类头。参数没有固定标准但有几个经验值学习率 1e-3 起步batch size 32 或 64epoch 50 到 100隐藏层 128 或 256。# slice_level_model/main.py 里的关键参数段 config { lr: 1e-3, # 学习率太大不收敛太小训练慢 batch_size: 32, # 显存不够就降到 16 epochs: 80, # 早停可以设 patience10 hidden_dim: 128, # 图卷积隐藏层维度 dropout: 0.3, # 防止过拟合 num_layers: 3, # GNN 层数太深会过平滑 }逻辑说明num_layers别设太大图神经网络层数一多节点表示会趋同反而掉点。dropout在切片数据量不大时很有用。训练时留意 loss 曲线如果 train loss 降但 test 不降就是过拟合加 dropout 或减层数。这里要提醒一句深度学习有不确定性训练出来的数据和论文里可能有出入。这不是代码问题是随机种子、数据划分、硬件差异共同导致的。我一般会跑三次取平均别拿一次结果下结论。3.3 训练失败的排查顺序训练跑不起来按这个顺序查先看数据路径对不对再看切片文件能不能正常加载然后看节点嵌入维度是否和模型输入匹配最后看显存。常见报错是KeyError多半是某个节点在 w2v 模型里没有对应向量检查min_count是不是设大了。还有CUDA out of memory降 batch size 或换小模型。4. 漏洞解释模型GNNExplainer 与 PGExplainer 怎么接4.1 解释模型的加载逻辑解释部分可以选择改进过的 GNNExplainer 或 PGExplainer。GNNExplainer 跑benchmar/kernal/pipeline.py参数在args.py里改。加载分三步先加载带解释的切片也就是预处理阶段完成节点嵌入的切片再加载漏洞检测模型路径在 args 里设指向训练保存的 ckpt 文件最后指定输出重要节点的路径。# benchmar/kernal/args.py 里的关键配置 parser.add_argument(--data_dir, typestr, default/data/slices/slice_pdg) parser.add_argument(--ckpt, typestr, default/data/model/best.ckpt) parser.add_argument(--out_nodes, typestr, default/data/explain/important_nodes.json) parser.add_argument(--explainer, typestr, defaultgnnexplainer)逻辑说明data_dir是切片目录ckpt是检测模型权重out_nodes是解释结果输出。explainer切换 GNNExplainer 和 PGExplainer。PGExplainer 的代码结构类似改路径即可不用大动。4.2 解释结果与实例对照interpre_example目录里给了 RQ3 中实例的源代码、切片 dot 文件和解释结果行号。这是验证解释效果最直接的材料拿解释模型输出的重要节点对照源代码行号看是不是真的定位到了漏洞相关代码。# preprocess/intrepre_effect.py 评估解释效果 import json # 先执行 lineinfo_dict.py 从 lineinfo 得到行号映射 with open(/data/explain/lineinfo_dict.json) as f: line_map json.load(f) with open(/data/explain/important_nodes.json) as f: important json.load(f) # 对比重要节点对应的行号和真实漏洞行 hit 0 for node in important: line line_map.get(node) if line in true_vul_lines: hit 1 print(命中率:, hit / len(important))逻辑说明lineinfo_dict.py先把节点 id 映射到源代码行号intrepre_effect.py再算命中率。true_vul_lines就是前面vul_lines字典里的真实漏洞行。命中率越高解释越准。注意这一步依赖前面的 json 文件路径别写错。5. 避坑与常见问题dot 加载、路径、嵌入维度5.1 dot 文件加载报错现象跑切片或解释时加载 dot 文件报解析错误提示格式不对。 原因Joern 导出的 dot 里可能包含特殊字符或转义问题Python 的 dot 解析库读不了。 解决执行preprocess/dot_fix.py它会清洗 dot 文件里的非法字符重新输出一份可加载的版本。我一般会在切片前统一跑一遍别等报错再补。5.2 路径硬编码导致找不到文件现象脚本跑一半报FileNotFoundError但文件明明在。 原因这套源码很多脚本里路径是写死的比如normalization.py、train_w2v.py、main.py你改了目录但没改脚本里的常量。 解决全局搜一遍/data/或C:\\这类硬编码路径逐个替换。建议用grep -rn /data/ preprocess/ slice_level_model/快速定位。5.3 节点嵌入维度不匹配现象训练时报 shape 不匹配比如Expected input dim 128 but got 64。 原因w2v 训练时vector_size设了 64但模型配置里hidden_dim或输入维度写的是 128。 解决两边对齐。要么改 w2v 的vector_size要么改模型输入维度。改完重新训练 w2v 并重新嵌入别只改一边。5.4 训练结果和论文差距大现象自己跑出来的准确率比论文低不少。 原因数据划分不同、随机种子不同、Joern 版本差异导致图结构不同。 解决固定随机种子多跑几次取平均检查 Joern 版本是否和项目说明一致。如果还是差很多看切片数量是不是对得上可能预处理阶段漏了文件。5.5 解释结果行号对不上现象解释模型输出的重要节点映射回源代码行号后和真实漏洞行完全错位。 原因lineinfo_dict.py没先执行或者 lineinfo.json 和切片不是同一批生成的。 解决确保lineinfo_dict.py在intrepre_effect.py之前跑且 lineinfo.json 和 pdg.dot 来自同一次 Joern 导出。别混用不同批次的文件。6. 进阶技巧把解释结果落成可复现的验证流程解释模型跑完拿到important_nodes.json很多人就停了。但我一般会多做一步把解释结果和interpre_example里的实例对照做成一个可复现的验证脚本。这样换数据集或换模型时能快速判断解释效果有没有退化。具体做法是先固定一批有真实漏洞行标注的切片跑解释模型算命中率存成 csv。下次改模型参数或换解释器重跑同一批对比命中率变化。这个流程不复杂但能避免「感觉变好了」这种玄学判断。# 批量验证解释效果 import json import csv CASES /data/explain/cases.txt # 每行一个切片名 OUT_CSV /data/explain/eval_result.csv rows [] with open(CASES) as f: cases [l.strip() for l in f if l.strip()] for case in cases: # 每个 case 对应一份 important_nodes 和 lineinfo_dict with open(f/data/explain/{case}_nodes.json) as f: nodes json.load(f) with open(f/data/explain/{case}_lineinfo.json) as f: line_map json.load(f) with open(f/data/explain/{case}_true_lines.json) as f: true_lines set(json.load(f)) hit sum(1 for n in nodes if line_map.get(n) in true_lines) rows.append({case: case, hit: hit, total: len(nodes), rate: hit / len(nodes) if nodes else 0}) with open(OUT_CSV, w, newline) as f: writer csv.DictWriter(f, fieldnames[case, hit, total, rate]) writer.writeheader() writer.writerows(rows)逻辑说明cases.txt里放你要验证的切片名每个切片对应三份 json解释输出的重要节点、节点到行号的映射、真实漏洞行。算完写 csv方便对比。参数上注意line_map.get(n)可能返回 None真实行号集合里没有 None所以不会误判命中。还有一个技巧是控制切片扩展跳数。我试过 1 跳、2 跳、3 跳1 跳上下文太少解释经常定位到无关行3 跳引入太多噪声命中率反而降。2 跳在多数场景下比较稳但不同项目可能要微调。这个参数在slice_preocess/main.py里改完重新生成切片再跑检测和解释。从那以后我每次换数据集都强制走一遍「切片跳数对比 → 检测准确率 → 解释命中率」这条链不靠单次结果下结论。希望帮到你。本文还有配套的精品资源点击获取
返回列表