异构DoS攻击下的预设时间控制仿真与MATLAB实现
简介:面向异构DoS攻击下预设时间控制的MATLAB仿真代码包,主要为研究网络化控制、多智能体系统或安全控制方向的学者及研究生,提供从典型工况到混合复杂情景的仿真验证工具。代码包按任务场景划分多个模块,涵盖典型版本、预设时间触发、周期切换拓扑、PT与PWT组合、规避策略以及PT与规避协同控制等情景,方便进行基准对照与多模块联合测试。压缩包共81个文件,以36个m脚本为主体,配合27张jpg静态结果图、12个gif动态演示以及3个fig图形文件,直观展示状态轨迹、观测误差和输出误差变化;另含md说明文档与def、prj工程配置,整体约53.23MB,目录结构清晰、便于按模块快速定位。目前已有51人学习下载。使用者可直接运行相应主程序与功能函数,快速复现异构DoS攻击下的预设时间控制效果,节省建模与调参时间,适合做论文复现、课程设计或相关课题预研的参考。
1. 异构DoS攻击下的预设时间控制:仿真的目标与适用场景
把异构DoS攻击、预设时间控制、MATLAB仿真代码这三个词放在一起,通常是在做一个网络化控制系统方向的控制方案验证:传感器把状态测量值发给控制器,中间信道被攻击者用多种方式持续干扰——周期性阻塞让你收不到包,随机丢包让你不确定哪个包可信,欺骗注入直接把测量值换成攻击者编造的假数据。控制器的目标不是简单地稳住系统,而是保证状态在一个预先指定的时间 T 内收敛到原点附近,并且这个 T 与初始状态、与攻击时间序列都无关。这篇文章给出一套能在 MATLAB 里直接运行的预设时间控制仿真代码,包含异构攻击生成器、时变增益控制律、绘图和排查记录。适合正在做安全控制、网络化控制方向的研究生和工程师,也适合想快速评估预设时间控制这个方向是否值得投入的从业者。
2. 异构DoS攻击建模与预设时间控制律:参数先定明白再写代码
动手写 MATLAB 代码之前,最值得花时间的是把攻击模型和控制器形式定清楚。这一步不清,后面跑出来的曲线只能“看起来像那么回事”,换一组参数或换一台机器结论就变。我在仿真里把异构DoS攻击拆成三种可叠加的机制,再配合一个带时变增益的预设时间控制律,下面分别说清楚各自的数学形式和参数选择逻辑。
2.1 三种攻击模式:阻塞、欺骗、随机丢包的数学描述
“异构”不是指攻击流量大,而是指攻击机制不同源、时间尺度不同。我在仿真里最常用的三种 DoS 变体是:
- 周期性阻塞,也就是狭义的 DoS。攻击者在某个时间窗口内,按固定周期和占空比阻塞通信信道,控制器侧表现为“收不到新数据包”,只能使用上一次收到的值。数学上用一个开关信号 s(t) ∈ {0,1} 描述,s=0 表示信道被占。需要设置的参数是攻击窗口 [T_start, T_end]、阻塞周期 τ 和占空比 duty。
- 欺骗注入。攻击者有能力覆盖数据包内容,把真实测量 x 替换成自己构造的假值 x_fake。它不需要持续在线,而是在时间窗内以概率 p_dec 随机出手,很像真实场景里的中间人篡改。
- 随机丢包。信道质量差或者攻击者附带制造丢包,每个控制周期以概率 p_loss 丢失一个分组,控制器侧同样用零阶保持补位。
三种攻击同时存在时,控制器收到的测量值 x_hat 不再是简单的 x_true,而是被叠加处理过的信号。我常用的建模顺序是:先做欺骗注入,再做周期性阻塞,最后叠加随机丢包。注意这个顺序不是随便定的:欺骗产生的假值如果恰好赶在阻塞窗口之前到达,会被控制器当成正常值存入保持寄存器,接下来一个阻塞周期内控制器一直拿这个假值算控制量。这个耦合细节就是“异构”二字的真正含义,也是很多初版仿真会漏掉的地方。
| 攻击模式 | 控制模型 | 关键参数 | 仿真取值 |
|---|---|---|---|
| 周期阻塞 | x_hat = x_hold,当 s(t)=0 | T_start/T_end、τ、duty | 0.3s~3.5s、0.25s、0.5 |
| 欺骗注入 | x_hat = x_fake | 窗口 [a,b]、概率 p_dec | [0.8,1.8]、0.25 |
| 随机丢包 | x_hat = x_hold | 概率 p_loss | 0.08 |
x_hold 表示控制器上一次成功收到的测量值,也就是零阶保持器记住的那个值。这个值可能是正常值,也可能是被欺骗攻击污染过的假值,取决于攻击时序。仿真里我用 persistent 变量来保存它,第 3 章的 hdos_attack.m 里会看到具体实现。
2.2 预设时间控制律:为什么 k0 + k1/(T-t) 能给出确定收敛上界
预设时间控制在文献里也叫 prescribed-time control,和有限时间控制、固定时间控制的区别很关键:有限时间稳定的收敛时间上界通常依赖初始状态 x(0),而预设时间控制要求收敛时刻 T 是纯设计参数——不管初始状态多大(在有界范围内),也不管攻击序列怎么变(有界扰动下),状态都必须在 t=T 时进入指定残差集。
对线性被控对象 dot{x} = Ax + Bu,我采用的控制律形式是:
u(t) = -[k0 + k1/(T_p - t)] · K · x_hat(t),t < T_p
为什么这个形式能给出预设时间收敛?看 Lyapunov 函数 V = 0.5·x‘x。令 r(t) = k0 + k1/(T_p - t),闭环写成 dot{x} = (A - r(t)·B·K)x。忽略有界扰动项时,只要 k0 让 A - k0·B·K 稳定,并且 k1 足够大,就能让 V 的导数满足:
dot{V} ≤ -2V/(T_p - t)
解这个微分不等式得到 V(t) ≤ V(0)·[(T_p - t)/T_p]²,当 t 趋近 T_p 时 V 趋近于 0。收敛上界 T_p 出现在不等式右端,与初始状态 V(0) 完全解耦,这就是“预设时间”四个字的由来。
参数选择逻辑我一般是这样:k0 按常规极点配置或 LQR 来选,保证 t=0 时刻闭环至少是稳定的;k1 决定把状态“压向零”的强度,k1 太大会让 T_p 附近控制输入出现尖峰,太小则收敛残差大;T_p 是设计规格而不是调参对象——你希望系统几秒内完成收敛,就直接把 T_p 写成几。需要说明的是,以上推导对标称系统成立。存在有界测量攻击时,控制律里的 x_hat 带误差,扰动项会被 r(t) 在 T_p 附近放大,所以仿真里看到的不是严格零而是小残差。这个特性是预设时间控制在攻击场景下的固有代价,也是为什么后面要做多随机种子验证。
2.3 仿真场景参数表:系统、控制器与攻击的参数怎么定
这一节的参数会直接写进第 3 章的代码,先给一张完整的清单和选择依据。
| 参数 | 取值 | 选择依据 |
|---|---|---|
| A | [0 1; 3 1] | 开环有正实部特征值,能明显看出控制必要性 |
| B | [0; 1] | 单输入二阶系统,时域曲线直观 |
| K | [6 4] | 底稿反馈增益,A-B·K 特征值约 -1.5±0.87i |
| k0 | 2.0 | 保证初始时刻闭环稳定 |
| k1 | 1.5 | 与 T_p 搭配,T 附近增益适中 |
| T_p | 2.0s | 预设收敛时间,设计规格 |
| dt | 1e-3 | 固定步长,欧拉法的常用取值 |
| T_sim | 4.0s | 仿真总时长,大于 T_p 以观察收敛后行为 |
攻击窗口特意设成从 0.3s 一直持续到 3.5s,覆盖 T_p 前后各一段:这样既能看预设时间控制在攻击持续下是否仍在 2s 附近完成收敛,又能看收敛之后继续攻击会不会把系统重新打散。欺骗窗口放在 [0.8, 1.8],原因是这段时间正好是控制器增益 r(t) 快速增长的阶段,此时注入假值对收敛过程干扰最明显,适合用来暴露问题。
3. 从零搭 MATLAB 仿真主循环:攻击生成器、时变增益控制器与绘图
这一章进入可以抄作业的部分。完整仿真由四个文件组成:初始化脚本、攻击生成器函数 hdos_attack.m、主循环、绘图代码。为了阅读方便,我把初始化、主循环和绘图放在同一个主脚本里,攻击生成器单独存成函数文件。所有代码在 MATLAB R2019a 及以上版本测试可用,旧版本注意把 xline 换成 plot 竖线。
3.1 初始化与存储分配:先把系统、攻击参数和数组准备好
% prescribed_time_doS_sim.m % 异构DoS攻击下的预设时间控制仿真 clear; close all; clc; rng(2024); % 固定随机种子,保证结果可复现 %% 被控对象与控制器参数 A = [0 1; 3 1]; % 开环不稳定,二阶系统 B = [0; 1]; K = [6 4]; % 底稿状态反馈增益 k_base = 2.0; % 时变增益中的固定部分 k0 k_beta = 1.5; % 时变增益中的时变部分 k1 T_p = 2.0; % 预设收敛时间(秒) %% 异构DoS攻击参数 att.T_start = 0.3; % 攻击窗口起点 att.T_end = 3.5; % 攻击窗口终点 att.block_period = 0.25; % 周期性阻塞的周期 att.block_duty = 0.5; % 阻塞占空比 att.deception_win = [0.8 1.8]; % 欺骗注入时间窗 att.p_deception = 0.25; % 欺骗注入概率 att.p_loss = 0.08; % 随机丢包概率 att.fake_value = [0.6; 0.6]; % 欺骗攻击注入的虚假状态 %% 数值仿真参数 dt = 1e-3; % 固定步长 T_sim = 4.0; % 仿真总时长 t = 0:dt:T_sim; N = length(t); x0 = [1.5; -0.8]; % 初始状态初始化部分的要点有三个。第一,rng(2024) 必须放在所有随机过程之前,否则攻击生成器里的 rand 每次调用结果都不一样,仿真就没法复现。第二,att 是一个结构体,把攻击参数打包在一起,这样后续蒙特卡洛验证时只需要循环修改 att 的字段,不用改主循环。第三,K 和 k_base 分开设置是有意的:K 决定反馈方向,k_base 决定初始控制强度,调参时先动 k_base 再动 K,定位问题更快。
3.2 hdos_attack.m:异构攻击生成器的持久化保持逻辑
攻击生成器是整个仿真里最容易写错的文件。它要模拟“控制器收到的测量值”,而不是真实状态。我单独建一个函数文件,用 persistent 变量保存控制器侧的保持寄存器。
function [x_valid, att_id] = hdos_attack(x_true, t, att) % hdos_attack 异构DoS攻击生成器 % 攻击叠加顺序:欺骗注入 -> 周期阻塞 -> 随机丢包 % x_valid: 控制器实际收到的测量值 % att_id: 最后生效的攻击类型标识 % 0=正常, 1=随机丢包, 2=周期阻塞, 3=欺骗注入 persistent x_hold if isempty(x_hold) x_hold = x_true; % 首次调用用真实值初始化 end att_id = 0; x_valid = x_true; %% 1) 欺骗注入:时间窗内以概率 p_deception 覆盖测量值 if t >= att.deception_win(1) && t <= att.deception_win(2) if rand < att.p_deception x_valid = att.fake_value; att_id = 3; end end %% 2) 周期阻塞:信道被占,控制器只能使用上一次的值 if t >= att.T_start && t <= att.T_end if mod(t - att.T_start, att.block_period) < att.block_period * att.block_duty x_valid = x_hold; % 零阶保持 att_id = 2; end end %% 3) 随机丢包:伯努利丢包,丢包时也保持上次值 if rand < att.p_loss x_valid = x_hold; if att_id == 0 att_id = 1; end end %% 更新保持寄存器:阻塞时不更新,其他情况把本次结果存入 if att_id ~= 2 x_hold = x_valid; end end这个函数的逻辑说明值得细看。欺骗注入先执行,但如果在同一个控制周期里阻塞也生效,最终输出的是 x_hold 而不是假值——这符合物理直觉:被篡改的数据包要能到达控制器才会产生危害。真正危险的场景是:上一个周期欺骗成功,假值已经写进 x_hold,下一个周期进入阻塞窗口,控制器拿出来的 x_hold 就是那个假值。参数说明上,mod(t-att.T_start, att.block_period) < duty·period 是方波发生器,duty=0.5 表示每个周期内前一半阻塞、后一半放行;rand 每次调用都会消耗随机数序列,这就是为什么 rng 必须固定。函数末尾“att_id ~= 2 才更新 x_hold”的规则也解释了阻塞发生时保持寄存器不会被本次欺骗结果覆盖。
3.3 主循环:欧拉更新、预设时间增益与增益饱和
主循环是仿真引擎。我采用显式欧拉法做状态更新,原因是系统里存在不连续的攻击切换和状态保持逻辑,固定步长更容易控制攻击时序与积分步长的对齐;如果换成 ode45,反而要处理事件检测才能准确捕捉阻塞沿。
%% 存储分配 x = zeros(2, N); % 真实状态轨迹 x_hat = zeros(2, N); % 控制器收到的测量值 u = zeros(1, N); % 控制输入 att_flag = zeros(1, N); % 攻击标识 x(:,1) = x0; %% 主循环 for i = 1:N-1 % 1) 攻击生成:拿到控制器视角的测量值 [x_valid, flag] = hdos_attack(x(:,i), t(i), att); x_hat(:,i) = x_valid; att_flag(i) = flag; % 2) 预设时间时变增益 if t(i) < T_p k_t = k_base + k_beta / (T_p - t(i)); else k_t = k_base + k_beta / dt; % 等效为最后一步的增益 end k_t = min(k_t, 500); % 增益饱和,防止奇异 % 3) 控制律与状态更新 u(i) = -k_t * (K * x_valid); x(:,i+1) = x(:,i) + dt * (A * x(:,i) + B * u(i)); end % 补齐最后一个采样点的历史记录 x_hat(:,N) = x_hat(:,N-1); att_flag(N) = att_flag(N-1); u(N) = u(N-1);主循环有四个关键参数要解释。第一,k_t 在 t 趋近 T_p 时趋于无穷,这是预设时间控制的理论要求,但计算机数值无法表示无穷大,所以用 min(k_t, 500) 做饱和——只要 dt 足够小,状态在增益达到 500 之前已经被压到接近机器精度,饱和不会明显改变收敛性质。第二,t >= T_p 之后我把 k_t 固定为 k_base + k_beta/dt,等价于“最后一步的有效增益”,避免切换瞬间增益跳变再次激励系统。第三,状态更新用的是真实状态 x(:,i),而控制律用的是 x_valid,这两者的区别就是攻击引入的测量误差,也是残差的来源。第四,如果仿真中发现状态在 T_p 附近振荡,优先检查 dt 是不是太大,其次再检查 k_t 饱和上限是否过高。
3.4 绘图:状态、控制输入与攻击时间线三张子图
%% 绘图 figure('Position', [100 100 700 800]); subplot(3,1,1); plot(t, x(1,:), 'b', 'LineWidth', 1.5); hold on; plot(t, x(2,:), 'r', 'LineWidth', 1.5); xline(T_p, '--k', 'T_p', 'LineWidth', 1.2); legend('x_1', 'x_2', 'T_p', 'Location', 'best'); grid on; ylabel('状态'); title('预设时间控制下的状态轨迹'); subplot(3,1,2); plot(t, u, 'LineWidth', 1); xline(T_p, '--k', 'T_p'); grid on; ylabel('控制输入 u'); title('控制输入(注意 T_p 附近的增益尖峰)'); subplot(3,1,3); stairs(t, att_flag, 'LineWidth', 1.2); ylim([-0.5 3.5]); grid on; ylabel('攻击标识'); xlabel('时间 (s)'); title('攻击时间线:0=正常 1=丢包 2=阻塞 3=欺骗');读图时重点看三件事。第一,T_p 竖线处 x_1、x_2 是否进入你预设的残差带,比如 ±1e-3;第二,控制输入 u 在 t 接近 T_p 时是否出现尖峰,尖峰高度是否超出执行器量程;第三,攻击标识是否为 2 的时间段内状态出现明显台阶,如果是,说明保持寄存器里存的是被污染的测量值。这三张图对应预设时间控制的三个验证维度:收敛性、控制能量、攻击鲁棒性。xline 需要 MATLAB R2018b 以上,旧版本用 plot([T_p T_p], ylim, '--k') 代替。
4. 仿真避坑指南:增益爆炸、随机种子与欺骗攻击耦合排查
这套仿真我前后改过很多版,踩过的坑集中在四个方面。每一条都按“现象 → 原因 → 解决”来写,方便你对照排查。
4.1 状态在 T_p 附近跳 NaN:增益奇异与步长不匹配
现象:状态曲线在接近 T_p 时突然变成 NaN,图里出现断线,控制输入直接飞到 1e308。
原因:k_t 的理论值是 k_base + k_beta/(T_p - t),当 t 非常接近 T_p 时这个值超出 double 能表达的范围;更隐蔽的原因是显式欧拉法在这种大增益下不再稳定,增益越大,闭环特征值越远,欧拉的稳定步长上限 2/|λ_max| 越小,dt=1e-3 在大增益段可能不够小。
解决:给 k_t 加饱和上限 min(k_t, 500),这是第一道保险;把 dt 从 1e-3 缩小到 1e-4 对比一次,如果曲线明显变化说明步长仍在起作用;再不行就在状态更新里改用半隐式欧拉,把 A 矩阵的刚性部分隐式处理。我的经验是 500 的饱和配合 1e-3 步长对这类二阶系统已经足够,但如果你把 k_beta 调到 10 以上,就必须同时缩小步长。
4.2 换台机器结果就变:随机种子与 persistent 残留
现象:同一个脚本,第一次运行状态在 2s 收敛,第二次运行状态在 2s 后还有一个大的抖动;或者脚本连续跑两次,第二次的结果和第一次完全不同。
原因:攻击生成器里用了 rand,而主脚本只调用了一次 rng(2024),为什么第二次还不一样?问题出在 hdos_attack.m 开头的 persistent x_hold。第一次运行结束时 x_hold 保存的是第一次仿真最后几步的值,第二次运行时 isempty(x_hold) 为 false,保持寄存器带着上一次的残留数据直接开始工作。
解决:主脚本开头加 clear hdos_attack,强制清掉 persistent 变量;rng(2024) 放在 clear 之后。如果你做蒙特卡洛循环,注意在每次更换种子之前都要 clear hdos_attack,否则不同随机种子之间的攻击结果会被残留的 x_hold 串起来。这也是我最开始翻车的地方,建议直接写进自己的仿真模板里。
4.3 欺骗值混进保持寄存器:异构攻击耦合导致状态被带偏
现象:攻击标识为 2(阻塞)的时间段里,状态没有保持平稳,反而朝着某个方向匀速漂移,且漂移方向在每次仿真中一致。
原因:欺骗攻击在一个非阻塞周期成功注入假值,x_hold 被更新为假值;下一个阻塞周期控制器拿假值算控制量,控制律把系统推向错误方向。因为 x_hold 是 persistent 的,这个过程会持续到下一次成功接收到真实测量为止。这不是随机扰动,而是异构攻击的确定性危害。
解决:从建模上给控制器加一道测量校验,比如判断 x_valid 的范数是否超过物理上限,超过则丢弃并保持上一值;从仿真验证上,做一个关掉欺骗攻击的对照实验,如果残差显著变小,说明欺骗-阻塞耦合确实是状态偏移的主因。我一般会同时做这两件事:仿真里先确认机理,再在控制算法侧加校验逻辑。
4.4 预设时间 T_p 设太小:执行器饱和与理论失效
现象:T_p 从 2.0 改成 0.5 之后,收敛时间反而变长,甚至系统在 T_p 附近剧烈振荡,控制输入明显超出执行器量程。
原因:T_p 缩小意味着 k_beta/(T_p - t) 在更短时间内被放大到同样水平,控制输入峰值几乎按 1/T_p 的比例增长。如果被控对象带执行器饱和,实际系统在饱和区间运行,等效增益下降,预设时间性质不再成立。这是预设时间控制的物理边界,不是仿真 bug。
解决:仿真中加入执行器饱和,u_sat = max(min(u, 10), -10),再观察收敛性质;如果必须使用小的 T_p,需要同时增大 k_base、减小 k_beta,把“靠时变增益压收敛”改成“靠高基准增益压收敛”,但这样做会牺牲 T_p 附近控制输入的形状。判断标准是看饱和段占比,如果超过仿真总时长的 10%,说明 T_p 定得不合理,需要回退参数。
5. 验证预设时间性质:收敛时间测量与多初值蒙特卡洛检查
仿真跑通之后,不能只看一眼曲线就说“有效”。预设时间控制的核心承诺是“T 与初值无关”,这个性质必须用量化手段验证。我习惯做三层检查:单次收敛时间测量、固定增益对照、多初值多种子批量验证。
5.1 量化收敛时间:一个简单的阈值判定脚本
%% 收敛时间测量:状态二范数首次低于阈值的时间 threshold = 1e-3; idx = find(vecnorm(x, 2, 1) < threshold, 1); if isempty(idx) t_conv = inf; else t_conv = t(idx); end fprintf('收敛时间: %.4f s (阈值 %.0e)\n', t_conv, threshold);vecnorm(x, 2, 1) 按列计算状态向量的二范数,find 取第一个低于阈值的采样点。阈值取 1e-3 是经验值,对应“工程上认为已收敛”的精度;如果你的控制目标残差是 1e-4,就把阈值改成 1e-4。注意阈值不能小于数值积分本身的精度,否则 idx 可能为空,t_conv 变成 inf,这是正常的,说明该组参数下没有在仿真时间内达到预期精度。
5.2 对照组实验:固定增益与预设时间控制的收敛时间对比
把 k_t 改成固定值 k_base,其他参数和攻击序列全部保持相同,跑一次完整仿真,用同一个测量脚本记录收敛时间。固定增益下闭环特征值约 -1.5±0.87i,理论收敛时间在 2.5s 到 3s 之间;预设时间控制应该在 T_p 附近完成收敛。这个对比能直观说明时变增益项 k1/(T_p - t) 的价值——它不是锦上添花,而是把收敛时间从“由系统动态决定”变成“由设计参数决定”。
我在实际验证时会把两组仿真的状态轨迹画在同一张图上,并标注两条收敛时间竖线。如果固定增益组的收敛时间和预设时间组差距不明显,说明 k_base 本身已经足够大,时变项没有发挥应有作用,此时应该减小 k_base 或增大 k1,让两组结果拉开差距,才有说服力。
5.3 多初值与多攻击相位的批量验证
预设时间性质的严格说法是“收敛时间上界 T 与初始状态无关”,单组初值证明不了这一点。我通常在最后跑一组批量仿真:初值取 [-3, -1] 到 [3, 1] 区域内的几个代表性点,随机种子取 2024 到 2033 共 10 组,逐一统计收敛时间和最终残差。
%% 多初值+多随机种子批量验证 x0_list = [1.5 -0.8; -2.0 1.0; 0.5 0.5; 3.0 -1.0; -1.0 -1.5]; seed_list = 2024:2033; t_conv_all = zeros(length(x0_list), length(seed_list)); for i = 1:length(x0_list) for j = 1:length(seed_list) rng(seed_list(j)); clear hdos_attack; % 清除 persistent 残留 % 重新运行主循环(建议把主循环封装成函数) % t_conv_all(i,j) = measure_conv_time(...); end end fprintf('最大收敛时间: %.4f s\n', max(t_conv_all(:)));批量验证通过后,可以下结论说这套预设时间控制方案在给定攻击模型下满足设计指标。如果最大收敛时间明显超出 T_p,优先检查是不是增益饱和限制了大增益段的有效性,或者欺骗攻击的假值在 T_p 前最后一个阻塞周期内把状态重新踢出残差带。我自己的习惯是每改一次攻击参数就跑一遍这组批量验证,防止调出一套只在某一组随机种子下好看的仿真。希望这些方法能帮你少走我踩过的坑,把验证精力花在真正影响结论的地方。
本文还有配套的精品资源,点击获取