简介:MATLAB模拟复杂网络攻击的源代码包,面向网络安全研究者、复杂网络爱好者及学习网络抗毁性分析的高校学生。源码包以m主脚本为核心,演示构建小世界网络、无标度网络等典型模型,并实现随机攻击、度优先选择性攻击等策略,通过对比攻击前后网络拓扑与性能指标,帮助理解复杂网络在遭受攻击时的连通性、效率及鲁棒性变化。压缩包整体仅37KB,共3个文件:一个MATLAB脚本、一份doc说明文档和一个txt文本文件;说明文档交代实验思路,脚本可直接运行观察结果,适合动手验证。已有4821人浏览学习。借助该源码,读者可快速上手复杂网络攻防模拟,并在此框架上继续扩展网络生成算法、攻击模式或加入可视化输出,亦可作为相关课程设计和安全分析报告的基础参考。
1. matlab模拟复杂网络攻击源代码.zip:先搞清楚这个压缩包到底能做什么
第一次拿到 matlab模拟复杂网络攻击源代码.zip 这个包,多数人第一反应是里面是不是藏了个能一键打进内网的攻击工具。真打开过类似包的人都知道,恰恰相反。它在 MATLAB 环境里把 DDoS、蠕虫扩散、多阶段横向移动这类复杂攻击变成可重复运行的仿真实验,目的是观察攻击过程、研究防御策略、验证检测算法,而不是去碰真实系统。我在这个方向上做了不少时间,最大的体会是:MATLAB 处理邻接矩阵、事件时间线和批量流量生成非常顺手,能把一个抽象的攻击场景变成能出数值、能画图、能反复改参数的实验平台。适合做入侵检测研究的同学、安全课程设计的人,以及刚接触攻击建模的从业者。拿到这个包不要只盯着里面的数据文件和绘图脚本,重点应该放在攻击模型怎么落地成代码、参数怎么调、结果怎么解释,这是后面每一章要展开的事情。
2. 怎么把复杂攻击装进 MATLAB:三层架构、邻接矩阵与仿真时钟
2.1 先拆层:网络拓扑、攻击事件、流量输出各管一摊
仿真做过几次就会发现,所谓复杂网络攻击其实横跨三个差异很大的层次:网络本身长什么样、攻击行为在什么时候发生、发生后产生了哪些可观测流量。常见做法是把整套仿真拆成三层,各管一摊,改攻击强度时不用动拓扑,换拓扑时不用重写事件逻辑。我自己习惯的目录组织长这样。
attack_sim/ topology.m % 生成网络拓扑,返回邻接矩阵 events.m % 生成攻击事件时间线 traffic.m % 根据事件和时间线生成流量 features.m % 从流量里提取特征,给检测算法用 run_main.m % 入口:把上面几个模块串起来topology.m 负责回答“网络里有几个节点、谁和谁相连”,输出通常是一张邻接矩阵;events.m 负责回答“攻击在什么时候开始、持续多久、波及哪些节点”,输出是一张事件表;traffic.m 把前两者换算成带时间戳的流量序列,也就是每秒多少请求、多少字节。分层之后,调试某个环节时不需要翻遍所有代码。比如怀疑蠕虫模型算错了,直接单独跑 topology 和 worm 函数,不看流量输出。
分层还有一个容易被忽略的好处:方便替换模型。今天用无标度网络,明天想改成小世界网络,只需要替换 topology 函数内部实现,对外接口保持不变。攻击事件也一样,DDoS 和蠕虫的事件表结构完全一致,只是字段内容不同。这让后面所有特征提取、可视化和评估代码都不用跟着改。
2.2 用邻接矩阵描述攻击目标网络:一条命令算遍所有邻居
在 MATLAB 里描述一个网络最直接的数据结构就是邻接矩阵 A,A(i,j)=1 表示节点 i 和节点 j 之间有边。矩阵化的好处到后面会非常明显:想知道每个节点有多少个邻居,直接对矩阵按行求和;想知道哪些节点被感染节点包围,用一个矩阵乘以向量就全部算完。节点数到几千规模时,普通矩阵会吃掉大量内存,所以我会在一开始就声明成稀疏矩阵。
% 生成无标度网络:优先连接,让少数节点成为中心 n = 200; % 节点数 adj = sparse(n, n); % 稀疏矩阵,避免 n^2 内存占用 for i = 2:n deg = full(sum(adj, 2)); p = max(deg(1:i-1), 0.01); % 节点被选中的概率与度数成正比 p = p / sum(p); j = randsample(i-1, 1, true, p); adj(i, j) = 1; adj(j, i) = 1; % 无向边,镜像写入 end无标度网络是安全仿真里很常用的拓扑模型,因为它能模拟出少数关键服务器被大量普通节点环绕的现实结构。每加入一个新节点,就在已有节点里按度数加权随机连一条边,度数高的节点越来越容易被连到,最终形成少数枢纽节点。sparse 声明让 200×200 的矩阵在更大规模下依然轻量,改成 2000 节点时内存也不会立刻爆炸。randsample(i-1,1,true,p) 的前三个参数依次是抽样范围、抽几个、是否放回,p 是每个候选节点被抽中的概率。
有了邻接矩阵,传播类攻击的邻居计算就变成一次矩阵乘法:
infected = false(n, 1); infected([1 10 33]) = true; % 假设 1、10、33 是初始感染源 exposure = adj * infected; % 每个节点周围有多少个感染邻居这一小段是后面蠕虫模型的基础。adj * infected 做了什么事呢?infected 是 n×1 的 0/1 向量,矩阵乘法的结果里第 i 行恰好是节点 i 所有邻居中感染者的数量。不用循环,不用逐节点访问邻接表,一条命令算完。这也解释了为什么我坚持用矩阵而不是 cell 数组存邻居列表:后续模型代码会短很多,而且数值计算走的是编译好的底层库,速度远远快过 for 循环。
2.3 仿真时钟的取舍:事件驱动更贴近攻击逻辑
写仿真最早要定的一件事是时间怎么走。两种常见方案:固定时间片推进和事件驱动。纯按固定时间片推进最简单,每秒算一次状态,但攻击行为往往不是每秒都在变,比如扫描阶段前五秒密集发生,之后完全安静,固定时间片会浪费大量计算在无事件的时间段上。事件驱动则维护一个带时间戳的事件列表,仿真时钟直接跳到下一个事件发生的时间,效率高,但实现复杂一些。我的做法是两者混用:攻击行为层走事件驱动,流量输出层走固定时间片,因为最终要的流量序列本身就是按秒组织的。
% 最小事件表:列1 开始时间,列2 结束时间,列3 事件类型,列4 涉及节点数 events = [ 0, 10, 1, 50; % 类型1:扫描,前10秒扫50个节点 5, 60, 2, 20; % 类型2:DDoS,从第5秒持续到第60秒,20个攻击源 30, 90, 3, 5; % 类型3:挖矿扩散,从第30秒影响5个节点 ]; T = 100; % 总仿真时长,单位秒 traffic = zeros(T, 1); for t = 1:T active = events(:,1) <= t-1 & t-1 < events(:,2); traffic(t) = sum(events(active, 4)); end这段代码把事件表翻译成了每秒的流量强度。active 是个布尔向量,标识当前秒有哪些事件正在生效。需要注意边界条件的设计:开始时间取第 0 秒,而 MATLAB 数组下标从 1 开始,所以代码里用 t-1 对齐,保证第 1 秒对应事件表中的第 0 秒。结束时间用开区间,一个持续 10 秒的事件实际覆盖 0 到 9 秒,避免下个事件在第 10 秒开始时被上一个事件尾音干扰。这是仿真里很容易踩的边界坑,后面排查章节还会细说。
3. 复现典型攻击模型:MATLAB 实现与参数调节
3.1 攻击模型怎么选:三类主流模型的适用边界
不同攻击模型回答的问题完全不同,选错模型再调参都是白费力气。安全仿真里最常遇到三类,我把它们的特性列在一起对比。
| 模型 | 建模对象 | 核心参数 | 典型输出 |
|---|---|---|---|
| DDoS 流量生成 | 攻击源的请求行为 | 攻击源数量、请求速率、背景流量比例 | 每秒请求数序列 |
| 蠕虫传播 SIR | 节点状态转换 | 传播率 β、恢复率 γ、初始感染节点 | 易感/感染/恢复节点数量曲线 |
| 多阶段攻击编排 | 攻击步骤与横向移动 | 每阶段时长、涉及节点数、步骤依赖 | 事件日志、移动路径 |
DDoS 模型的重点是流量形态,它不关心节点内部状态,只关心网络入口看到的请求量变化。蠕虫模型的重点是节点状态在网络中的扩散,传染源周围的拓扑结构直接影响传播速度。多阶段攻击模型的重点是攻击步骤之间的先后关系和每一步的资源投入,它需要的不是流量数学,而是事件编排。做仿真之前先问自己一句:这次实验关心的输出是什么。如果是检测算法的误报率,选 DDoS 流量模型;如果是蠕虫传播速度和免疫策略,选 SIR;如果是攻击链还原和告警关联,选多阶段编排。
3.2 单源与分布式 DDoS 流量生成:从泊松过程到流量序列
DDoS 仿真的难点不在“把流量调大”,而在生成一个具备统计特征的请求序列。真实网络里每个攻击源的请求间隔近似服从指数分布,即单位时间内的请求数服从泊松分布。我会用事件间隔法先生成每个攻击者的请求时刻,再聚合到时间片上。
function traffic = gen_ddos(duration, attack_source, req_rate, rng_seed) % duration 仿真时长(秒),attack_source 攻击源数量, % req_rate 每个攻击源平均每秒请求数,rng_seed 复现用随机种子 rng(rng_seed); traffic = zeros(duration, 1); for src = 1:attack_source interval = exprnd(1.0 / req_rate, duration * 10, 1); t = cumsum(interval); % 累加得到每个请求的时刻 t = t(t < duration); % 丢弃超出仿真时长的部分 t = floor(t) + 1; % 对齐到秒级下标 t = max(t, 1); t = t(t <= duration); traffic(t) = traffic(t) + 1; % 对应秒的请求计数加一 end baseline = poissrnd(200, duration, 1); % 正常背景流量 traffic = traffic + baseline; endexprnd(1.0 / req_rate, duration * 10, 1) 生成的是指数分布的随机间隔,平均间隔是平均请求率的倒数。比如每秒请求率是 5,平均间隔就是 0.2 秒。乘上 10 是为了生成足够长的间隔序列,保证在时长范围内攒够请求数,生成后再用逻辑索引截断。floor(t) + 1 把浮点时间戳对齐到整秒下标,这也是流量序列必须做的事,因为后续特征提取是按秒窗口计算的。背景流量用 poissrnd(200) 生成均值 200 的泊松流量,这个数值可以根据实际场景改成任意量级。
调参时最容易忽视的是 req_rate 与背景流量的对比关系。如果攻击流量只有背景流量的两倍,检测算法很难区分;如果直接放大到一百倍,任何阈值算法都能检测出来,实验就没有区分度了。我一般会让攻击流量达到背景流量的 5 到 20 倍,留出足够的参数空间做实验。
3.3 蠕虫传播的 SIR 模型:状态转移与网络结构耦合
蠕虫传播模型把每个节点分成易感、感染、恢复三种状态,在给定网络拓扑上逐轮更新。这里的传播率 β 指的是“一个感染节点把病毒传给一个易感邻居的概率”,恢复率 γ 指的是“感染节点每轮恢复的概率”。多个感染邻居同时作用于一个易感节点时,直接用邻接矩阵乘以感染向量算出邻居数量,再算联合感染概率,一步到位。
function [sHist, iHist, rHist] = worm_sir(adj, beta, gamma, T, seed_nodes) % adj 邻接矩阵,beta 传播率,gamma 恢复率 % T 仿真轮数,seed_nodes 初始感染节点集合 n = size(adj, 1); S = true(n, 1); I = false(n, 1); R = false(n, 1); I(seed_nodes) = true; S(seed_nodes) = false; sHist = zeros(T, 1); iHist = zeros(T, 1); rHist = zeros(T, 1); for t = 1:T neighbors = adj * I; % 每个节点的感染邻居数 infect_prob = 1 - (1 - beta).^(neighbors); % 多个来源的联合概率 newI = rand(n, 1) < infect_prob & S; newR = rand(n, 1) < gamma & I; S = S & ~newI; I = (I | newI) & ~newR; R = R | newR; sHist(t) = sum(S); iHist(t) = sum(I); rHist(t) = sum(R); end end核心在第 6 行到第 8 行。neighbors 直接复用 2.2 节说的矩阵乘法,计算出每个节点周围有多少感染者。infect_prob 用的是多个独立感染事件至少发生一个的概率,如果有两个感染邻居,每个邻居传播概率是 beta,节点幸存的概率是 (1-beta)^2,所以感染概率是 1 减去这个幸存概率。这一步如果漏掉,直接用 neighbors * beta,传播速度会被明显高估,特别是高密度网络里特别明显。newI 是在易感节点里按概率抽签,newR 是在感染节点里按概率抽签,两句逻辑实现一轮状态转移。
这个模型的参数和网络结构耦合得厉害。同样 beta=0.1,在稀疏网络里可能传播很慢,在枢纽节点密集的无标度网络里却可能初期爆发。跑实验时不要只调 beta,还要记录网络的平均度、最大度这些结构指标,否则结果解释不清楚。T 的取值也要看网络直径,通常至少要跑几倍于网络直径的轮数,传播才可能进入平稳期。
3.4 多阶段攻击编排:用结构化事件表模拟完整攻击链
多阶段攻击和前面两类模型不一样,它模拟的不是单一机制,而是一条有依赖关系的动作链。扫描阶段完成后才可能进入利用阶段,提权失败后不会进入横向移动。所以我用一张结构化事件表,每条记录包含阶段名、开始时间、持续时长和涉及节点数。
stages = struct( ... 'name', {'recon'; 'exploit'; 'privesc'; 'lateral_mv'; 'exfil'}, ... 'start', [0 10 40 80 160], ... 'duration', [10 30 40 80 20], ... 'node_count', [50 3 1 5 1]); log_table = zeros(1000, 3); % 预分配日志矩阵 log_idx = 1; for k = 1:length(stages) for t = stages(k).start : stages(k).start + stages(k).duration - 1 log_table(log_idx, :) = [t, k, stages(k).node_count(log_idx > 0)]; log_idx = log_idx + 1; end end log_table = log_table(1:log_idx-1, :);每行的四个字段分别定义了攻击的阶段、什么时候开始、持续多久、涉及多少节点。比如 recon 阶段在 0 到 9 秒之间对 50 个节点做扫描,exploit 阶段在 10 到 39 秒只针对 3 个节点做漏洞利用。这样编排的好处是,后面做告警关联分析时可以直接按阶段编号过滤日志,分析哪一步耗时最长、哪一步最容易失败。
持续时长用开区间也是一个刻意的设计:第 10 秒开始时 exploit 介入,recon 在第 9 秒结束,两个阶段不会在同一时刻重叠,避免日志里出现归属不清的记录。这个代码里日志矩阵预先分配了 1000 行,防的是在循环里不断给数组追加导致性能崩掉。如果阶段很多或者时长很长,按实际需要把预分配行数放大即可。
4. 把仿真结果变成可用数据集:特征提取、导出与可视化
4.1 从原始流量里提取特征:为什么不能用裸流量直接训练
仿真跑完得到的是每秒请求数这样的一维时间序列,直接拿来做检测算法效果通常不好。原因很简单:单秒的流量没有上下文,一次偶然的突发和真正攻击的持续拉升在单点上看起来一样。常见做法是把它转换成滑动窗口统计特征,每个时间点用前面若干秒的数据计算均值、方差、最大值、异常计数等,这样的特征才有区分度。
窗口长度是这里最关键的参数。窗口太短,统计特征抖动剧烈,噪声压不住;窗口太长,攻击开始和结束的时刻被严重平滑,检测结果的定位精度变差。我一般从 10 秒窗口起步,观察检测效果后按 5 秒步长调整。窗口本质上是精度和稳定性的折中,没有绝对正确的值,要靠实验数据说话。
4.2 滑动窗口特征提取:一段可以直接落地的 MATLAB 代码
function feat = sliding_features(traffic, win) % traffic 每秒流量序列,win 窗口长度 N = length(traffic); feat = nan(N, 5); % 每行对应一秒,未满窗口的位置留 NaN for t = win+1 : N w = traffic(t-win+1 : t); % 取当前秒往前 win 秒 mu = mean(w); sd = std(w); feat(t, :) = [mu, sd, max(w), ... sum(w > mu + 3*sd), traffic(t)]; end end这段代码生成五个特征:窗口内均值、标准差、最大值、超过均值三倍标准差的次数、当前秒原始值。第三个特征能抓突发峰值,第四个特征能抓持续的异常抬升。用 NaN 填充开头不足窗口长度的部分,后面生成数据集时直接过滤掉 NaN 行,比用 0 填充干净,因为 0 会被当成正常数值进入模型。
窗口滑动的实现用了一个 for 循环,直接在测试集上跑数千秒的数据时性能也够用。如果要把代码用在几百万秒的长仿真上,可以把循环改成 conv 卷积来算滑动均值,但方差和阈值计数还是得用循环,所以我没有一上来就优化,确保代码可读性更重要。
4.3 导出 CSV 数据集与可视化:让攻击过程肉眼可见
特征提取之后,下一步是把特征和标签写进文件,供外部机器学习框架读取。我会把攻击发生的秒数标成 1,正常秒数标成 0,然后和特征矩阵一起写出去。
X = feat(~isnan(feat(:,1)), :); y = attack_label(~isnan(feat(:,1)), :); writematrix([X, y], 'attack_dataset.csv');writematrix 是 MATLAB 新版提供的快速导出函数,第一列为特征矩阵,最后一列为标签,CSV 文件可以直接被 Python 的 pandas 或 MATLAB 自带的 fitctree 读取。如果机器上还有更老的环境没有 writematrix,用 csvwrite 也能达到同样效果,只是速度会慢。
可视化这一步我建议在导出之前先做,因为很多数据错误肉眼一下就能看出来。最简单的可视化是把原始流量和攻击标签画在同一张图上。
figure; subplot(2,1,1); plot(traffic); hold on; plot(attack_label * max(traffic), 'r-'); legend('traffic', 'attack'); subplot(2,1,2); plot(adj, '.', 'MarkerSize', 1);上面的代码画出两幅图:上边是流量曲线叠加攻击区间,下边是网络拓扑散点。流量图能立刻看出攻击注入的时间点是否正确,拓扑图能看出网络结构是否符合预期。我每次在跑正式实验前都会先出这两张图,目检确认没有异常再继续后面的步骤,这个习惯救了不少次因为事件表时间错位导致的数据污染。
5. 仿真翻车避坑与常见问题排查:现象、原因、对策
5.1 现象:仿真跑一半卡死,内存占用一路飙到顶
仿真规模稍微拉大,比如把节点数从 200 改成 2000,很多人的 MATLAB 直接卡死。原因通常有两个:一是邻接矩阵没有用 sparse 声明,2000×2000 的双精度矩阵占了约 32MB,如果再存几十个这样的变量,内存很容易爆;二是在循环里不断用 end+1 给数组追加数据,每追加一次 MATLAB 都要重新分配整块内存,复杂度是 O(n²)。
解决的办法是提前规划好数据的形态。拓扑矩阵一律用 sparse(n,n) 声明,日志类矩阵在循环前先预分配一个足够大的零矩阵,记录完再截断。另外在代码开头加一句 clear 变量操作,把上一次调试留下的缓存清掉。写仿真默认这个习惯,内存问题基本消失。
5.2 现象:换个机器重新跑,同样的代码结果完全对不上
这个现象十有八九是随机数种子没有固定。MATLAB 的 rand、randn、randsample 默认使用全局随机流,每次启动 MATLAB 的种子都不一样,所以换机器、换版本、甚至重开一次脚本,结果都会变。仿真实验最怕这个,因为论文和报告里没法复现数据。
解决方法是所有用到随机数的入口统一加一个随机种子参数。我在每个模型的函数签名里都放一个 rng_seed 参数,主脚本开头统一用 rng(20240601) 这样的固定值。同一个种子跑出来的结果完全一致,换种子就是换一组实验数据。如果要跑多组对照实验,就设计一个种子列表,每组实验记下用的是哪个种子,报告里写清楚,别人复现时按种子列表跑即可。
5.3 现象:检测算法准确率虚高,高到不敢信
第一次跑通整套仿真后,很多人拿自己做出来的数据集训练分类器,准确率往往冲到 99% 以上。这个数字看起来漂亮,其实是仿真数据太干净了。攻击流量和背景流量的统计特征差距非常明显,分类器随便学两下就能区分,而真实网络里的攻击会藏在一堆复杂噪声里。
解决方向有两个。第一,在流量生成阶段加入更真实的噪声,比如把背景流量的均值调成周期性波动,模拟白天高、夜间低的业务规律,而不是固定均匀的泊松流。第二,刻意缩小攻击流量和背景流量的差距,把攻击倍率放低,让攻击的发生只引起统计特征的渐变而不造成断崖式跳变。检测算法能在这种条件下仍然有效,才说明它真正捕捉到了攻击的模式,而不是靠暴力区分。
5.4 现象:蠕虫传播结果不符合直觉,传播一轮就全感染了
用 SIR 模型时,如果把传播率设成 0.3,很多新手直觉认为每个节点每轮有 30% 概率被感染,所以传播应该慢慢推进。实际跑出来第一轮就几乎全部感染,非常不符合预期。原因是忽略了我前面说的联合感染概率:一个节点周围如果有八个感染邻居,它的感染概率是 1 - (1-0.3)^8,算出来是 94%,而不是 30%。在密集网络里,只要感染源附近节点多,一轮内就会被多个方向同时攻击。
解决方法是理解 beta 的物理含义是“单个感染源对单个邻居的传播率”,而不是“节点的整体感染率”。如果希望控制整体传播速度,需要先统计网络的平均邻居数,然后把 beta 调小。比如平均度是 10,想要每个节点每轮约 20% 被感染的概率,beta 不是 0.2,需要解 1-(1-beta)^10 = 0.2,beta 大约 0.022。算好再设参,模型行为才能符合预期。
6. 仿真结果验证:三个能救场的收尾技巧
6.1 用小算例手算核对,先证明模型没有系统性错误
调参调到头昏的时候,最有效的验证方法反而是回到一个极小的网络上。我会把节点数改成 4,拓扑设计成一条链,手动算出第一轮传播后各个节点的状态,再跑仿真代码对比。代码结果和手算结果一致,才说明模型逻辑没有系统性错误。这个习惯看起来笨,但能直接定位到问题在状态转移还是随机抽签。大网络上的结果无法手算,只能信任逻辑,所以逻辑的正确性必须在最小算例上预先确认。
6.2 参数与随机种子的存档习惯,让实验可复现
每次跑完一组实验,我会顺手把这次用的所有参数写在一个 config.mat 或 CSV 里,包括节点数、攻击源数量、传播率、恢复率、窗口长度、随机种子、以及当时的 MATLAB 版本号。存完参数再跑一遍同样代码,确认输出一致才归档。这个习惯起初只是怕自己忘参数,后来发现跨版本复现时版本号非常关键,老版本生成的随机数序列可能和新版本不同,别人拿到代码后如果有版本对照信息,就不会在复现问题上卡太久。
6.3 预演整条链路:不改模型,先跑通最小流程
最后做一个端到端冒烟测试:把节点数设成 20,仿真时长设成 30 秒,跑通拓扑生成、攻击注入、特征提取、CSV 导出这四个环节。这个测试不关心结果好坏,只确认每个模块之间接口一致。很多问题其实不是模型写错,而是两个函数之间的变量名对不上,或者矩阵维度差了一列,这些小问题在最小规模试跑时一两分钟就能暴露。等最小的链路跑通,我再把参数拉回正式规模,后面的调试就省心很多。
做仿真这几年,我最大的教训是不拿大网络当第一调试对象,改参数前先想清楚这次要看哪个量、改哪个参数、结果是否符合物理直觉。数据能跑出来不等于实验设计成立,多问自己一句结果为什么长这样,往往能发现模型逻辑里的漏洞。这套流程我自己每次做攻击仿真都先用一遍,希望能帮到你。
本文还有配套的精品资源,点击获取