ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

西门子PLC动态加密功能块:S7-1200/1500数据保护实战

西门子PLC动态加密功能块:S7-1200/1500数据保护实战 做西门子PLC编程的人大概率都遇到过这样的场景非标设备交付之后甲方拿着博途软件把程序一上传里面的配方、密码、关键工艺参数全变成表格摊在桌面上核心价值等于免费赠送。博途自带的块保护确实能挡一部分人但它防的是“看梯形图”防不了“在线监控DB”、防不了“上传后离线分析”。我今天要说的动态加密功能块程序就是运行在S7-1200/1500里的一个SCL函数块专门做应用层数据加密关键数据在PLC内部以密文存储用到哪一步才解到哪一步用完立刻清掉。这篇内容适合做设备保护、远程授权、MES/SCADA通讯的工程师也适合正在研究西门子PLC程序架构的入门者。1. 动态加密功能块到底解决什么问题1.1 博途自带的程序保护为什么还不够很多工程师第一次接触保护功能用的是博途里的“Know-How Protection”专有技术保护。这个功能确实有用给FB、FC加个密码之后别人打开你的项目只能看到块接口看不到内部逻辑哪怕把程序上传上来也是一堆被锁住的“黑盒”。所以不少设备商觉得这样已经够了程序都看不见了还有什么好担心的但实际交付一次你就明白了块保护保护的是“逻辑”不是“数据”。你的程序算法别人打不开但PLC里那些DB块的实时值、保持值、配方参数大多数情况下还是会被在线监控一览无余。举个例子你的设备里有一个工艺温度参数存在全局DB的某个偏移量里操作工打开变量表甚至不用看懂梯形图直接就能看到这个参数现在是180.5度。更麻烦的是密码HMI登录用的管理员密码如果以明文放在DB里别人一上载程序离线打开DB密码直接等于摆在那里。所以我的观点是程序保护和数据保护必须分开考虑。块保护是第一道门挡住“看逻辑”的人动态加密功能块是第二道门挡住“看数据”的人。两道门都装上才能说这设备的保护是完整的。1.2 动态加密的适用场景和边界动态加密功能块最典型的应用场景有这么几类第一类HMI权限密码。操作员级别、工程师级别的密码不能以明文存在PLC里存进去的必须是加盐之后的散列值而且校验时要带防暴力破解逻辑。第二类配方和工艺参数。关键参数以密文存放在掉电保持区设备运行时临时解密到内存里使用用完清零。这样做的好处是就算别人把存储卡读出来拿到手的也是一堆乱码。第三类对外通讯数据。PLC和第三方上位机、MES、SCADA做Modbus TCP或OPC UA通讯时如果有些关键数据不想在网络上被直接抓包看到可以在PLC侧先加密再发出。第四类设备租赁和远程授权。设备到期后锁机授权码用动态加密方式生成不依赖固定的明文密码别人很难仿造到期逻辑。但也要说清楚边界。这个功能块是为了提高门槛不是为了做到绝对不可破解。PLC本身没有可信计算环境密钥因子只要在PLC内部理论上都能被逆向所以你的目标应该是让普通工程师看不懂让竞争对手需要付出极高的时间成本。如果你面对的是专业逆向团队那就不是靠一个SCL功能块能解决的事了。1.3 “动态”两个字具体体现在哪里所谓“动态”核心是密钥不是写死在程序里的一个常数。很多年前我见过有人做保护程序里写了一个固定的异或密钥比如所有数据都和16#AA异或一遍这种方式确实能让新手看不懂但只要被人拿到程序反编译一下密钥马上就暴露了而且一个密钥能解所有数据一旦泄露全线崩溃。动态加密的思路完全不同。密钥由设备信息、运行次数、时间戳等多个因子参与生成同一段明文每次加密出来的密文都不一样。就算别人在线监控看到一次密文甚至用录像机把整个运行过程录下来他也无法推算出下一次的密文变化规律。更实用的是在防暴力破解这个环节“动态”意味着不是固定锁死几秒钟而是根据错误次数动态延长锁定时间第一次输错锁2秒第五次输错可能锁5分钟直接把穷举的路堵死。所以“动态”不是一个噱头它解决的是固定密钥的“单点失效”问题。密钥因子跟着设备走换一台设备同样的明文加密出来结果完全不同这才是设备防抄的核心逻辑。2. 算法选型和加密流程设计PLC里哪种方案最实用2.1 选型思路为什么不直接上AES一提加密很多人第一反应就是AES。确实S7-1500性能强S7-1200在最新固件下也能算但我在实际项目里基本不推荐在PLC内部直接跑AES算法。原因不复杂就两个算力和成本。一个S7-1200 CPU做一次128位AES的块加密如果数据量是几百字节扫描周期会肉眼可见地拉长。设备上有通讯、有PID、还有轴控制你不可能把CPU的时间都烧在加密上。而且AES用SCL写出来代码量很大移植到不同固件版本还得反复调试维护成本不低。更重要的是工业现场大多数场景根本不需要这种级别的防护。我们防的不是情报机构而是“用博途打开程序看一眼”的同行以及“拿U盘拷个程序就走”的竞争对手。所以我的推荐是轻量级组合算法CRC16校验 滚动异或 位置置换 盐值。这个组合在PLC上执行效率高代码短而且已经足够把绝大多数人挡在门外。如果项目真的对安全性有硬性要求更合理的做法是S7-1500配合西门子官方的安全库或者干脆让上位机做AESPLC只负责数据分发不要把算法硬塞到PLC里。2.2 加密流水线CRC16 滚动异或 位置重排我用一个例子来说明这套组合是怎么工作的。假设要把一串明文数据变成密文完整流程分四步第一步加盐。在明文头部或尾部拼接一段随机数盐值盐值可以来自当前时间、运行计数器的低位这样即使两段数据明文相同最终密文也不同。第二步滚动异或。用一个初始种子生成密钥流逐字节和明文异或。关键在“滚动”这两个字每处理完一个字节密钥就变化一次这样密文里不会出现重复模式。第三步位置置换。把异或后的字节按一个固定步长重新排列位置。比如数据长度为256步长取37因为37和256互质所以按37的步长跳着取可以把所有位置都遍历一遍打乱顺序。第四步追加校验值。对最终密文计算CRC16并追加到数据尾部。解密的时候先校验CRC如果数据在传输或存储过程中被改了一个字节立刻就能发现而不是解出一堆乱码还不自知。解密就是倒过来先校验再逆置换再滚动异或最后去掉盐值。这一套流水线写起来并不复杂但已经包含了“混淆”和“扩散”两种基本思想比单独用异或可靠得多。2.3 滚动密钥怎么生成才靠谱滚动密钥是整个算法的心脏这部分设计不好前面那些步骤再复杂也白搭。我常用的方案是线性同余生成器公式长这样新密钥 (旧密钥 × 常数A 常数C) MOD 2^16这个算法在C语言里很常见在PLC里用SCL实现也不难每一步只需要做一次乘法、一次加法和一次取余扫描周期开销可以忽略。关键在于初始种子从哪来。我建议的种子由三部分组成设备硬件标识、掉电保持运行计数器、本次调用序号。硬件标识保证不同设备上的密钥不同运行计数器保证每次上电后的密钥序列不同调用序号保证同一段数据在运行期间重复加密时结果也不同。加密和解密必须使用同一个种子所以种子要么保存在掉电保持区要么能被固定规则重新计算出来。这就要注意了种子不能直接用当前时间因为如果甲方改了系统时间你的密钥就对不上了。有工程师会问动态密钥这么折腾解密怎么保证一定成功答案就在种子的“可复现性”。只要设备没换、运行次数没被清零、调用序号一致解密时算出来的密钥流和加密时完全一样数据自然能还原。这也是我为什么强调种子要可控、可迁移不能拍脑袋随机。3. 功能块接口设计与程序框架从需求到代码的桥梁3.1 输入输出引脚怎么设计功能块封装得好不好直接决定别人用起来想不想骂人。我见过有人把加密功能块的接口设计了二十多个引脚光看命名就要研究半天这种块从设计上就已经失败了。合理的做法是控制引脚数量把常用参数暴露出来内部细节全部藏着。我一般这样定义引脚参数数据类型方向说明bExecuteBOOL输入上升沿触发一次加解密任务iModeINT输入0加密1解密iDataLenINT输入待处理数据长度字节数arrDataARRAY[0..255] OF BYTEInOut明文/密文数据区dwUserKeyDWORD输入调用方提供的用户密钥qDoneBOOL输出任务完成状态qBusyBOOL输出任务执行中qErrorBOOL输出错误标志iErrIdINT输出错误代码dwChecksumDWORD输出本次数据CRC校验值bExecute用上升沿触发避免每个扫描周期把同一组数据反复加密一遍。iDataLen必须做合法性检查超过数组边界直接置qError绝对不能让越界把CPU搞停机。dwUserKey是给调用方自定义“盐”用的不同设备可以提前烧录不同的用户密钥这样就算两套PLC程序完全一样同一个密码加密出来的结果也不一样。3.2 背景数据块和静态变量规划为什么用FB而不是FC因为加密功能块必须有内部记忆。当前密钥状态、运行步骤、CRC查表、临时数据缓冲区这些都需要在多个扫描周期之间保持FB的背景数据块刚好可以做这件事。FC没有自己的背景DB虽然能在全局DB里存状态但那样就会污染全局命名空间项目一复杂就乱了。FB内部静态变量我一般规划成这么几类状态控制类state、idx、step用于状态机切换和循环计数。密钥类keyCur、keySeed、failCount保存当前密钥流和错误计数。查表类crcTable[0..255]、bTableReadyCRC计算用的常量表。临时数据类arrScratch[0..255]存放中间运算结果。最重要的一个设计原则是所有明文临时数据都要放在非保持区。加解密过程中产生的明文用完必须清零。如果放在保持区断电之后掉电保持区里还残留着明文别人上载保持区数据就能直接恢复出来。这是我在项目里吃过亏才总结出来的。3.3 状态机设计加密大块数据不拖慢扫描周期很多第一次写加密块的人习惯在一个扫描周期里用一个FOR循环把整个数据处理完。256字节的数据循环256次里面再做CRC、异或、置换在S7-1200上可能直接把扫描周期拉长到几十毫秒。如果设备上还有别的实时控制任务这个时间开销完全不可接受。解决办法是状态机分片。把一次加解密任务拆成多个扫描周期执行每个周期只处理一小块数据。我用CASE语句实现一个简单的五状态状态机IDLE空闲状态等待bExecute上升沿。INIT初始化密钥流、校验数据长度准备临时缓冲区。PROCESS循环处理数据每次进入该状态处理固定字节数没处理完就继续处理完跳到DONE。DONE置位qDone把临时数据清零回到IDLE。ERROR长度异常或校验失败置位qError回到IDLE。这样做的好处很明显每次进入PROCESS状态最多执行一小段循环单周期耗时被限制在可接受范围内设备整体的扫描时间几乎不受影响。4. SCL实操核心加解密代码与可参考的调用方式4.1 CRC16-Modbus查表法核心代码CRC16计算有两种写法一种是逐位计算代码简单但慢另一种是查表法速度快适合PLC这种对扫描周期敏感的环境。我习惯用查表法表在功能块首次运行时生成一次存在背景DB里。下面这段代码是在TIA Portal V16以上的SCL环境里写的个别类型转换指令如果和你的博途版本不一样用版本自带的CONV转换指令替换即可// 首次运行生成CRC16-Modbus查表 IF NOT #bTableReady THEN FOR #i : 0 TO 255 DO #crc : #i; FOR #j : 0 TO 7 DO IF (#crc AND 16#0001) 16#0000 THEN #crc : (#crc SHR 1) XOR 16#A001; ELSE #crc : #crc SHR 1; END_IF; END_FOR; #crcTable[#i] : #crc; END_FOR; #bTableReady : TRUE; END_IF;生成一次表只要跑到255对整个扫描周期的影响只在首次上电那一次。后面计算CRC的时候直接查表就行#crc : 16#FFFF; FOR #n : 0 TO #iDataLen - 1 DO // 取当前字节参与异或低8位作为查表索引 #idx : (#crc XOR BYTE_TO_WORD(#arrData[#n])) AND 16#00FF; #crc : (#crc SHR 8) XOR #crcTable[#idx]; END_FOR;注意CRC初始值是16#FFFF多项式和Modbus标准一致。追加校验的时候把#crc拆成高字节和低字节放到数据尾部。4.2 滚动异或与位置置换滚动异或的核心代码比较短重点在密钥每处理一个字节就要更新一次。我用线性同余生成密钥流常数选的是16#4C11和16#5A5A这个组合在16位宽度下表现不错足够让密文没有明显周期性// 初始化密钥流 #keyWord : WORD(#dwSeed AND 16#FFFF); // 对每个字节做滚动异或 FOR #n : 0 TO #iDataLen - 1 DO // 更新密钥 #keyWord : (#keyWord * 16#4C11 16#5A5A) AND 16#FFFF; // 取低字节做异或 #xorVal : WORD_TO_BYTE(#keyWord AND 16#00FF); #arrData[#n] : #arrData[#n] XOR #xorVal; END_FOR;位置置换我用的是跳步法。假设数据长度是256步长取37因为37和256互质所以按37步长跳着取能且只能访问到全部256个位置。这样做的好处是不需要额外存储一张256字节的置换表省内存// 正向置换把scratch区数据打乱顺序写入输出区 #j : 0; FOR #n : 0 TO #iDataLen - 1 DO #arrOut[#n] : #arrScratch[#j]; #j : (#j 37) MOD #iDataLen; END_FOR;解密的时候要做逆置换逻辑反过来写就行。这里有一个细节如果数据长度不是和37互质置换就会漏掉部分位置所以调用前必须检查iDataLen。我的功能块里强制要求长度只能选256、128、64这类2的幂这样和37一定互质省了很多麻烦。4.3 整体调用示例HMI密码校验与配方加解密举两个实际调用场景来说明功能块怎么用。场景一HMI登录密码校验。密码不在PLC里存明文存的是加盐后的密文。操作工在HMI上输入密码PLC先把输入的明文密码加上固定的盐值调用动态加密功能块算出密文再和DB里保存的密文做比较。这样即使DB被读出来攻击者拿到的也是一串不可逆的密文// 用户从HMI输入的密码存放在inputPwd #inputCopy : #inputPwd; // 加密输入密码 #cryptoBlock( bExecute : #trigPwdLogin, iMode : 0, iDataLen : 256, arrData : #inputCopy, dwUserKey : #deviceKey, qDone #pwdDone, qError #pwdErr, iErrId #pwdErrId ); // 如果加密结果等于DB里保存的密码密文则认为密码正确 IF #pwdDone AND #inputCopy[0] #dbPwdHash[0] AND #inputCopy[1] #dbPwdHash[1] THEN #loginOk : TRUE; END_IF;场景二配方参数加密存储。配方里的关键温度、压力值存在掉电保持DB区读取配方时先解密再使用// 从保持区读出密文配方 #recipeCipher : #dbRecipeCipher; // 解密到临时区 #cryptoBlock( bExecute : #trigRecipeRead, iMode : 1, iDataLen : 256, arrData : #recipeCipher, dwUserKey : #deviceKey, qDone #recipeDone, qError #recipeErr, iErrId #recipeErrId ); // 解密完成后立即使用用完后清零临时区 IF #recipeDone THEN #currentSetpoint : #recipeCipher[0] * 256 #recipeCipher[1]; END_IF;注意使用完recipeCipher后必须清零至少要把这个中间数组全部置0不留明文残留。4.4 在OB1和中断OB中调用时的注意事项功能块设计完了调用位置也得讲究。我的经验是数据量小、扫描周期不敏感时直接在OB1里调用。比如密码校验这种一次性操作不会影响实时性。数据量大时比如一次要处理几百字节的配方最好放到定时中断OB比如OB32或者循环中断OB里执行用状态机分片处理。这样加解密任务不会挤占OB1的正常循环时间。和Modbus TCP这类通讯任务配合时特别注意通讯保持区不要和加密处理区重叠。只要通讯程序在加密程序运行的过程中去读数据区就有可能读到“加密到一半”的脏数据导致通讯报文校验不通过。我的做法是单独留一块“邮箱区”加密完成后再整体搬运到通讯区。5. 可靠性设计上电、掉电、防篡改的细节5.1 上电初始化与随机种子管理动态加密功能块用的种子不能是真正“随机”的必须是可复现的。我在项目里的做法是初始化在OB100里做功能块首次运行前先把种子里需要的运行计数器、设备硬件标识读出来组装成初始种子。有一个容易踩的坑设备运行计数器必须放在掉电保持区。如果放在普通DB区设备断电再上电计数器清零种子突然回到初始状态历史保存的密文全部解不开。保持计数器还有一个好处每次设备上电都会变化这样一来设备在客户手里放了一个月后再上电密文依然能解开但外人即使拿到旧密文也算不出新密文。S7-1500可以比较方便地读取设备的一些标识信息参与种子计算S7-1200要根据具体固件和硬件情况来看能读就参与读不到就用保持计数器加用户密钥来兜底。这个细节直接影响程序的可移植性功能块写出来不能只能在你自己这台PLC上跑换一台设备算法逻辑要能迁移。5.2 掉电保持区的数据布局加密数据和密钥因子放哪里是有讲究的。基本原则是三句话密文放保持区密钥因子放保持区明文临时区放非保持区。保持区存储空间在S7-1200上默认是有限的S7-1500虽然大一些但也不能无限用。如果配方表特别大全表加密会把保持区撑爆。这种时候我的建议是只加密关键参数比如配方里影响质量的核心温度、压力、时间其余普通参数不加密。加密区域越小出错的概率越低维护也越方便。如果现场需要在线升级功能块程序或者是更换存储卡最好在设计阶段就在程序里预留一个“导出明文”的维护入口用管理员权限触发把密文解密成明文导出后再做硬件更换。等新设备装好再重新加密写回。否则换完硬件发现数据解不开那损失就不是几百行代码的事了。5.3 防暴力破解与锁定机制密码校验类的应用必须做防暴力破解。最简单也最有效的方法是错误次数递增锁定。错误计数放在掉电保持区断电重启也不能清零。我用的策略是连续错误1到3次每次锁2到10秒。连续错误4到8次每次锁30到120秒。连续错误9次以上锁定300秒。锁定期间哪怕密码输入正确也不执行身份校验。锁定时间公式可以写成这样lockTime min(2^(failCount-1), 300)前几次增长比较温和后期指数膨胀直接把穷举工具的耐心耗光。管理员解锁的话需要一个单独的维护权限操作把计数器和锁定状态复位。有一点要特别注意锁定逻辑要放在密码校验程序的最前面并且锁定的判断条件要影响最终结果不能只弹个提示框。不然别人连上PLC反复发送密码请求你的锁定形同虚设。6. 常见问题与排查技巧实录6.1 解密出来全是乱码这个是我被问得最多的问题。解密乱码八成原因是加密和解密时使用的密钥流不一致。排查思路按顺序来先检查数据长度。加密时给的是256解密时不能给成128长度不一致直接导致密钥流错位。再检查种子。加密和解密用的dwUserKey、运行计数器、设备标识是不是同一个。特别是程序升级后如果运行计数器被清零或者更换过存储卡种子就会变。最后检查置换参数。数据长度如果不是2的幂步长37和数据长度不互质置换就会丢字节解出来的数据自然是乱的。我的建议是写一个自检功能块加密一段已知数据然后立刻解密比对结果是否一致。现场排查的时候先用固定密钥模式关闭动态因子测试确认功能块基本逻辑正常再逐步打开动态因子这样能快速缩小范围。6.2 加了加密块之后扫描周期暴涨如果发现设备加完加密功能块之后扫描周期从5毫秒涨到50毫秒十有八九是你在OB1里写了一个256字节的完整加密循环而且是一次性跑完。前面我讲的状态机分片就是为这个场景准备的。检查方法很简单博途在线监控OB1的运行时间如果加密任务尽量放到了中断OBOB1时间还是很高那就看中断OB的执行频率是不是太高了。循环中断OB如果设置成1毫秒触发一次一次加密任务又要跑2毫秒那肯定要出大事。把中断周期加到10毫秒或20毫秒通常就能解决。还有一种情况是CRC查表没生效每次调用都在重新生成256项的表。查一下bTableReady是不是被意外复位了这个标志位如果没保持住每一轮都在建表扫描时间自然会暴增。6.3 在线监控还是看到了明文有人问我明明加密了为什么在线监控里还是能看到明文这个问题问出来多半是监控的时机不对。解密过程不是瞬时的解密后的数据要写到临时区然后被业务程序读取使用这个窗口期如果刚好被在线监控抓到明文就暴露了。对策有几个层面。明文临时区不要放到全局DB或M区里最好放在FB的静态变量里这样至少不会出现在主程序的全局数据列表里。使用完明文后立刻清零不让明文在PLC里多待一个扫描周期。还有就是调试阶段不要把变量表里那些自动监控的变量全选上只保留真正需要的监控项减少误判风险。重要提醒不要用M区存明文。M区在全项目里是共享的在线监控和程序调试都把M区当成重点区域看明文放M区等于把密码写在门锁旁边。6.4 更换存储卡或修改时间后数据解不开这种情况多发生在用“时间”或“硬件唯一标识”直接参与种子计算的程序里。时间变了密钥就变了历史密文自然解不开。硬件标识也一样换一张存储卡标识变了密钥跟着变。我的建议是密钥因子要做分层设计。底层因子必须是稳定且可迁移的比如掉电保持的运行计数器和用户密钥上层因子才考虑硬件标识或时间戳而且上层因子要能通过授权参数表进行配置。这样更换硬件后只要把授权参数表迁移过去密钥流还能复现数据不至于丢。从项目管理的角度操作流程也要规范化。设备更换CPU或存储卡之前先通过维护入口把关键数据解密导出更换完成后重新加密写回。不要等到设备已经在客户现场跑起来了才想起历史数据没备份那才是真的麻烦。最后说一点个人体会。我最早做这个功能块的时候脑子里想的全是“加密越强越好”结果算法堆了好几层现场同事改一个密码要拿着一堆工具倒来倒去最后不光效率低还容易出错。踩过几次坑之后我学乖了工业现场的加密重点是挡住顺手抄数据和误操作的人而不是和顶级专业人士硬刚。博途的块保护管逻辑动态加密功能块管数据密钥因子设计得简单、可复现、可迁移运维的时候不折腾这才是真正能长期用的方案。
返回列表