在西门子S7-1200/1500 PLC的结构化文本(ST)编程中,保护关键工艺参数(如PID设定值、电机限幅、配方常数)不被非授权修改,是自动化系统安全设计的重要一环。ST语言本身不提供内置加密库,但可通过异或(XOR)加密这一轻量、可逆、无状态的位运算,在资源受限的PLC环境中实现高效参数混淆。该方法无需额外硬件、不增加扫描周期负担,且密钥完全由用户控制,适用于防止HMI误操作、工程调试时的临时锁定、或基础级防篡改场景。
以下指南全程使用TIA Portal V18(兼容V16–V20),所有代码均可直接粘贴至FB/FC的ST编辑器中运行,无需调用库函数或外部指令。
一、理解异或加密原理:为什么它适合PLC
异或运算是二进制层面的逻辑操作,满足三个核心性质:
- 自反性:
A XOR B XOR B = A
即对同一数据连续两次用相同密钥异或,结果还原为原始值。 - 可交换性:
A XOR B = B XOR A - 无进位/无溢出:每个位独立计算,不产生进位,不会因数据长度变化导致结果异常。
因此,加密过程即:加密后值 := 原始值 XOR 密钥
解密过程即:原始值 := 加密后值 XOR 密钥
该过程在ST中仅需一行语句,执行时间恒定(通常≤0.1 μs),且支持任意整型(INT、DINT、UDINT)和字节数组(ARRAY[0..n] OF BYTE)。不适用于浮点数直接加密——因IEEE 754格式中符号位、指数位、尾数位含义不同,直接异或会破坏数值语义;必须先转为整型表示(如REAL_TO_DINT)再处理,并严格配套转换回写逻辑。
二、准备工作:定义密钥与数据结构
密钥必须满足两个条件:固定性(每次加解密使用相同值)、隐蔽性(不硬编码在HMI或DB注释中)。推荐将密钥存储于只读DB的私有区域,并通过FB封装访问逻辑。
-
创建密钥DB(KeyDB)
- 新建数据块
DB_Key,属性设为“优化的块访问”关闭(确保符号地址可寻址); - 添加静态变量:
Key1 : UINT := 16#A5F3; // 掩码1,建议16位非零值,避开全0/全1 Key2 : UDINT := 16#DEAD_BEEF; // 掩码2,用于DINT类参数 - 将
DB_Key的“访问权限”设为“读写 – 仅限程序内部”,并在“属性 > 保护”中勾选“阻止从HMI/OPC UA读取”。
- 新建数据块
-
定义待保护参数的数据结构
不要逐个变量加密。统一使用结构体(STRUCT)打包相关参数,提升维护性与一致性:TYPE ST_ProtectedParams : STRUCT MaxSpeed_rpm : INT; // 最大转速(工艺关键) TempSetpoint_C : REAL; // 温度设定值(需特殊处理) AlarmThreshold : DINT; // 报警阈值(整型) RecipeID : STRING[8]; // 配方编号(字符串需按字节处理) END_STRUCT END_TYPE⚠️ 注意:
STRING在S7中实际为ARRAY[0..7] OF BYTE(首字节存长度),因此需按字节逐位异或;REAL必须先转为DINT再加密,否则解密后无法还原为有效浮点数。
三、编写加密FB:EncryptParam_FB
新建功能块 EncryptParam_FB,接口如下:
| 变量名 | 类型 | 方向 | 说明 |
|---|---|---|---|
bEncrypt |
BOOL |
输入 | TRUE=加密,FALSE=解密 |
uDataIn |
UDINT |
输入 | 待处理的无符号32位整数(用于DINT/REAL转换后) |
uKey |
UDINT |
输入 | 加密密钥(从DB_Key传入) |
uDataOut |
UDINT |
输出 | 加密或解密后的结果 |
bDone |
BOOL |
输出 | 执行完成标志(上升沿有效) |
ST代码实现(完整可复制):
// EncryptParam_FB - ST代码区
VAR
static_bLastEncrypt : BOOL;
static_uLastKey : UDINT;
END_VAR
// 检测模式切换(避免重复触发)
IF bEncrypt <> static_bLastEncrypt OR uKey <> static_uLastKey THEN
uDataOut := uDataIn XOR uKey;
bDone := TRUE;
static_bLastEncrypt := bEncrypt;
static_uLastKey := uKey;
ELSE
bDone := FALSE;
END_IF
✅ 优势:
- 使用静态变量缓存上一次密钥与模式,仅在参数变更时执行XOR,杜绝扫描周期内重复计算;
bDone提供明确执行反馈,便于上层逻辑同步调用。
四、处理不同类型参数的实操步骤
1. 加密/解密 INT 或 DINT 类型(如 MaxSpeed_rpm, AlarmThreshold)
直接调用 EncryptParam_FB,密钥选用 DB_Key.Key1(UINT)或 DB_Key.Key2(UDINT),注意类型扩展:
// 示例:加密 MaxSpeed_rpm(INT → 转为UDINT再运算)
Encrypt_Speed(
bEncrypt := TRUE,
uDataIn := UDINT#(stParams.MaxSpeed_rpm), // 符号扩展为UDINT
uKey := DB_Key.Key2,
uDataOut => uEncryptedSpeed,
bDone => bSpeedEncDone
);
// 解密时,将结果强制转回INT:
IF bSpeedEncDone THEN
stParams.MaxSpeed_rpm := INT#(uEncryptedSpeed); // 自动截断高位,安全
END_IF
2. 加密/解密 REAL 类型(如 TempSetpoint_C)
必须经过 REAL ↔ DINT 转换,且全程使用 DINT 运算:
// 加密REAL
uTempAsDINT := REAL_TO_DINT(stParams.TempSetpoint_C);
Encrypt_Temp(
bEncrypt := TRUE,
uDataIn := UDINT#(uTempAsDINT),
uKey := DB_Key.Key2,
uDataOut => uEncryptedTemp,
bDone => bTempEncDone
);
// 解密REAL(严格顺序不可颠倒)
IF bTempEncDone THEN
uDecryptedDINT := DINT#(uEncryptedTemp); // 先转回DINT
stParams.TempSetpoint_C := DINT_TO_REAL(uDecryptedDINT);
END_IF
🔑 关键提醒:
REAL_TO_DINT默认采用“舍入到最近偶数”,若需截断,改用REAL_TO_DINT_TRUNC(TIA V17+);务必保证加解密使用完全相同的转换函数。
3. 加密/解密 STRING[8](如 RecipeID)
按字节逐位异或,密钥需循环使用(密钥长度 < 字符串字节数时):
// 假设 stParams.RecipeID 是 STRING[8],底层为 ARRAY[0..7] OF BYTE
// 定义局部变量:
arBytesIn : ARRAY[0..7] OF BYTE;
arBytesOut : ARRAY[0..7] OF BYTE;
i : INT;
// 拆包字符串为字节数组(首字节为长度,后7字节为字符)
arBytesIn[0] := LEN(stParams.RecipeID);
FOR i := 1 TO 7 DO
arBytesIn[i] := BYTE#(stParams.RecipeID[i]);
END_FOR;
// 循环异或(使用Key1逐字节)
FOR i := 0 TO 7 DO
arBytesOut[i] := arBytesIn[i] XOR BYTE#(DB_Key.Key1 MOD 256); // 取Key1低8位作字节密钥
END_FOR;
// 重组字符串
stParams.RecipeID := ''; // 清空
stParams.RecipeID[0] := arBytesOut[0]; // 写入长度
FOR i := 1 TO 7 DO
stParams.RecipeID[i] := CHAR#(arBytesOut[i]);
END_FOR;
✅ 此方案兼容任意STRING[n],只需调整数组范围与循环上限。
五、集成到主逻辑:双态保护机制
为防止单次调用失效,建议在参数写入路径上部署双检查机制:既加密存储,又在读取时校验完整性。
-
加密写入流程
当HMI提交新参数时:- 校验输入合法性(如
MaxSpeed_rpm∈ [0..3000]); - 调用加密FB处理各字段;
- 写入加密后值到受保护DB(如
DB_MachineParams); - 设置“已加密”标志位(如
DB_MachineParams.bEncrypted := TRUE)。
- 校验输入合法性(如
-
安全读取流程
在控制算法中读取参数前:- 检查
bEncrypted标志; - 若为
TRUE,则调用解密FB加载明文到工作变量; - 若为
FALSE,触发报警并停机(表明参数被绕过加密直接写入)。
- 检查
// 主程序片段(OB1)
IF DB_MachineParams.bEncrypted THEN
// 解密所有字段到临时结构体 stWorkingParams
DecryptAllParams(); // 封装了前述各类解密调用
// 后续控制逻辑使用 stWorkingParams.xxx
ELSE
// 紧急处理
DB_Alarm.AlarmCode := 1001;
DB_Alarm.AlarmText := 'PARAMS_NOT_ENCRYPTED';
MachineState := STATE_STOP;
END_IF
六、增强安全性:密钥动态化(进阶)
静态密钥存在被离线分析风险。可升级为运行时生成密钥,例如:
- 使用系统时钟低8位:
Key := BYTE#(TIME_OF_DAY() MOD 256); - 绑定硬件标识:
Key := WORD#(SYS_GET_HW_ID(1))(需启用系统函数); - 多因子组合:
Key := (DB_Key.Key1 * WORD#(CYCLE_TIME_MS())) XOR DB_Key.Key2。
⚠️ 注意:密钥必须在加解密全程保持一致。若用于多周期参数,需将生成的密钥缓存于静态变量,并在加密/解密FB中复用。
七、测试验证清单(必做)
| 测试项 | 方法 | 预期结果 |
|---|---|---|
| 加密-解密往返 | 对INT#1234加密后立即解密 |
结果仍为1234 |
| 边界值测试 | 加密INT#-32768、INT#32767 |
解密后值不变,无溢出警告 |
| REAL精度验证 | 设TempSetpoint_C := 99.9 → 加密→解密 |
结果为99.9(允许IEEE浮点微小误差,如99.89999) |
| 字符串长度变化 | 写入"R1"(长度2)→ 加密→解密 |
解密后仍为"R1",非乱码 |
| 断电保持 | 修改参数→加密→断电重启→读取 | 解密后值正确,DB内容未被HMI明文覆盖 |
八、局限性与规避建议
- 不防固件级攻击:PLC程序可被读取,密钥终将暴露。若需高等级防护,应结合S7-1500的“安全集成”功能或专用加密模块。
- 不替代访问控制:本方案不阻止HMI修改密文,仅确保明文不被直接读取。必须配合TIA Portal的“块保护密码”与CPU的“保护等级”设置(推荐设为“完全保护”)。
- 禁止用于安全相关参数:如急停阈值、安全门限位。功能安全参数必须遵循IEC 61508,本方案无认证资质。
九、完整代码模板(可直接导入)
// FB_EncryptManager - 封装全部加解密逻辑
FUNCTION_BLOCK FB_EncryptManager
VAR_INPUT
bEncrypt : BOOL;
stParamsIn : ST_ProtectedParams;
END_VAR
VAR_OUTPUT
stParamsOut : ST_ProtectedParams;
bSuccess : BOOL;
END_VAR
VAR
// 中间变量(略,见前述各节)
END_VAR
// 主逻辑(精简版)
bSuccess := TRUE;
// 加密MaxSpeed
Encrypt_Speed(bEncrypt := bEncrypt, uDataIn := UDINT#(stParamsIn.MaxSpeed_rpm), uKey := DB_Key.Key2, uDataOut => uEncSpeed);
IF bEncrypt THEN
stParamsOut.MaxSpeed_rpm := INT#(uEncSpeed);
ELSE
stParamsOut.MaxSpeed_rpm := INT#(uEncSpeed);
END_IF;
// (其他字段同理...)
// REAL处理(关键!)
uTempDINT := REAL_TO_DINT(stParamsIn.TempSetpoint_C);
Encrypt_Temp(bEncrypt := bEncrypt, uDataIn := UDINT#(uTempDINT), uKey := DB_Key.Key2, uDataOut => uEncTemp);
IF bEncrypt THEN
uDecDINT := DINT#(uEncTemp);
stParamsOut.TempSetpoint_C := DINT_TO_REAL(uDecDINT);
ELSE
uDecDINT := DINT#(uEncTemp);
stParamsOut.TempSetpoint_C := DINT_TO_REAL(uDecDINT);
END_IF;
暂无评论,快来抢沙发吧!