文章目录

ST数据加密:在ST中实现简单的异或加密保护参数

发布于 2026-03-19 04:28:46 · 浏览 6 次 · 评论 0 条

在西门子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),且支持任意整型(INTDINTUDINT)和字节数组(ARRAY[0..n] OF BYTE)。不适用于浮点数直接加密——因IEEE 754格式中符号位、指数位、尾数位含义不同,直接异或会破坏数值语义;必须先转为整型表示(如REAL_TO_DINT)再处理,并严格配套转换回写逻辑。


二、准备工作:定义密钥与数据结构

密钥必须满足两个条件:固定性(每次加解密使用相同值)、隐蔽性(不硬编码在HMI或DB注释中)。推荐将密钥存储于只读DB的私有区域,并通过FB封装访问逻辑。

  1. 创建密钥DB(KeyDB)

    • 新建数据块 DB_Key,属性设为“优化的块访问”关闭(确保符号地址可寻址);
    • 添加静态变量:
      Key1 : UINT := 16#A5F3; // 掩码1,建议16位非零值,避开全0/全1
      Key2 : UDINT := 16#DEAD_BEEF; // 掩码2,用于DINT类参数
    • DB_Key 的“访问权限”设为“读写 – 仅限程序内部”,并在“属性 > 保护”中勾选“阻止从HMI/OPC UA读取”。
  2. 定义待保护参数的数据结构
    不要逐个变量加密。统一使用结构体(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. 加密/解密 INTDINT 类型(如 MaxSpeed_rpm, AlarmThreshold

直接调用 EncryptParam_FB,密钥选用 DB_Key.Key1UINT)或 DB_Key.Key2UDINT),注意类型扩展:

// 示例:加密 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],只需调整数组范围与循环上限。


五、集成到主逻辑:双态保护机制

为防止单次调用失效,建议在参数写入路径上部署双检查机制:既加密存储,又在读取时校验完整性。

  1. 加密写入流程
    当HMI提交新参数时:

    • 校验输入合法性(如MaxSpeed_rpm ∈ [0..3000]);
    • 调用加密FB处理各字段;
    • 写入加密后值到受保护DB(如DB_MachineParams);
    • 设置“已加密”标志位(如DB_MachineParams.bEncrypted := TRUE)。
  2. 安全读取流程
    在控制算法中读取参数前:

    • 检查 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#-32768INT#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;

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文