ARTICLE DETAIL

资讯详情

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

S7-1200/1500动态加密授权功能块:设计思路与SCL实现全解析

S7-1200/1500动态加密授权功能块:设计思路与SCL实现全解析 这两年接过不少西门子S7-1200/1500的项目十有八九的客户都会提同一个需求能不能做一套动态加密功能块程序让设备只能在指定的PLC上跑授权到期自动锁机程序被拷走也跑不起来。这个需求听起来玄乎拆开看其实就是一套自研的授权与校验逻辑。这篇文章我就把这个功能块从设计思路、SCL代码到踩过的坑完整抖出来想给自家设备加“防盗锁”的按这个路子走基本能落地。1. 先拆概念S7-1200/1500动态加密功能块到底在加密什么1.1 “动态加密”不是西门子标准库而是一类自研授权逻辑很多人一听到“动态加密”四个字以为博途里有个现成的加密功能块拖进来设置个密码就能用。这里必须先澄清一个认知西门子官方并没有提供名为“Dynamic Encryption”的标准功能块。博途自带的块保护叫“专有技术保护”Know-how protection它解决的是“上载后别人能不能看到你的梯形图/SCL代码”的问题本质是静态密码保护。而行业里常说的“动态加密功能块程序”是工程师基于SCL/STL自己写的一套授权校验逻辑典型包含设备唯一标识采集通常是CPU序列号、注册码/解锁码的动态生成与比对、运行许可控制三大部分。之所以叫“动态”是因为每次校验的输入不是写死在程序里的固定常量而是CPU序列号、当前时间、运行次数这类会变化的变量。同样的注册码换一台PLC、过了授权期限统统失效。所以接下来所有内容都是围绕如何手写这套自研逻辑展开的。搞清楚这个前提你才不会被网上那些玄乎的“加密神器”带偏。1.2 三个典型业务场景锁机、收尾款、防抄方案我接触过的项目里动态加密功能块基本都在解决三种现实问题第一个是设备分期付款。客户只付了首付设备先拉去用但到了约定时间点设备自动锁定想继续用就得联系厂家付清尾款换解锁码。很多做非标设备的兄弟应该深有体会设备发出去收不回钱的情况太常见了机械锁容易被拆程序锁相对更体面。第二个是方案防抄。整套设备程序里最值钱的是工艺参数和核心逻辑你用专有技术保护把代码藏起来之后别人确实看不到了但人家可以把整个项目上载下载到另一台同型号PLC里照跑不误。动态加密把程序“绑定”到特定CPU的序列号上换一台PLC跑不起来才能拦住这种整体搬运。第三个是临时授权。试机、借用、展会演示的设备给一个固定使用期限到期自动失效。省得派人去现场“拆机”也能防止客户把试用设备当正式设备一直拖下去。1.3 S7-1200和S7-1500的能力边界决定方案怎么选实话实说S7-1200和S7-1500都能做动态加密但能力边界差异明显方案选型必须提前想清楚。从指令集看S7-1200没有官方AES/DES这类对称加密库S7-1500同样没有现成的加密指令所以常规做法是自研CRC32或者XXTEA这类轻量级算法。S7-1500的字符串处理和数组运算性能更强可以做更复杂的多轮混淆而S7-1200的CPU资源有限算法规模要控制尤其不能把大数组查表这种操作放在每个扫描周期里跑。从读取设备标识看S7-1200从V4.0固件开始可以用Get_IM_Data指令读IM0数据里面带CPU序列号S7-1500同样支持读取速度和数据结构完整度都更好。如果项目里用的是很老的S7-1200固件版本得先确认是否有这个指令。从实时时钟看S7-1200断电后靠超级电容维持RTC一般能撑十几天到二十天时间太长会丢S7-1500加电池模块或缓冲模块会更可靠。这一点直接决定“时间锁”方案敢不敢用。后面第4节我会专门讲这个坑。还有一个容易被忽略的点扫描周期。CRC32加时间比对如果每个扫描周期都跑一遍S7-1200的扫描周期会被明显拉长。所以动态加密功能块绝不应该放在OB1的每个循环里全程执行要么放到定时中断OB里节流执行要么在程序里加一个“每N秒执行一次”的判断。2. 动态授权FB总体设计三层结构和一整套授权状态机2.1 三层架构设备身份层、授权校验层、运行控制层动手写代码之前先把架构理清楚。我建议把整个动态加密功能块拆成三层各管各的事调试的时候能少掉一半头发。第一层是设备身份层。它的职责是拿到“这台PLC是谁”的证据。最通用的做法是用Get_IM_Data读取CPU序列号如果现场条件不允许读序列号也可以用厂家自己写入保持区的一个设备编号。前者好处是换PLC自动失效防挪机后者好处是逻辑简单但防不了别人把整个项目连DB一起搬走。我的经验是优先用CPU序列号。第二层是授权校验层。厂家的离线授权工具拿到序列号和授权截止日期之后用特定算法生成注册码PLC内的功能块用同样的算法算一遍两者比对一致才放行。这一层的关键是算法一致性后面第3节详细写。第三层是运行控制层。校验结果不是一个简单的布尔量就完了它要管理一套授权状态机分别处理未授权、已授权、试用期、授权过期、非法篡改等状态。不同状态对应不同动作未授权可以跑30分钟试机授权过期每小时停机5分钟并亮黄灯非法篡改直接锁机并记录次数。状态机的好处是把逻辑边界划清楚不会出现“到底要不要停机”这种模糊状态。2.2 授权状态机的设计细节状态机的状态定义我一般这样枚举状态含义触发条件设备行为0 未授权从未输入过有效注册码上电且授权标志位为0允许试机N小时HMI提示输入注册码1 已授权注册码校验通过且在有效期内校验通过且当前日期截止日期全功能开放2 试用期没有正式授权但处于首次试机时间内首次上电时间试用时长当前时间全功能开放HMI显示剩余试用时间3 授权过期超过截止日期当前日期截止日期且曾经授权过锁定工艺功能每小时解锁5分钟4 非法篡改检测到时钟回拨或数据异常历史最大时间戳当前时间立即锁机需厂家远程解锁码恢复5 永久锁定连续多次非法篡改非法篡改次数3永久锁机只能返厂处理状态转换的逻辑集中在授权FB内部输出一个枚举或整数型状态值给HMI显示。设备的行为动作则由主程序根据这个状态值去控制不要在FB内部直接写一堆M输出那样复用性会很差。2.3 授权数据块的设计原则公开区、隐藏区、保持区授权FB配套的数据块是整个方案的命脉布局我习惯分三个区公开区给HMI读写包括输入序列号显示或者自动读取后的显示、输入注册码的变量、授权状态值、剩余天数、剩余试机时间。这些变量HMI能读能写方便现场人员操作。隐藏区是FB静态变量或者私有DB包括盐值数组、历史最大时间戳、失败尝试次数、锁定标志、授权校验通过标志。这些变量只允许程序内部访问HMI不需要看到。在博途里如果用的是一般DB且没有勾选“从HMI可见”HMI就访问不到如果用的是优化访问DB注意在变量属性里把“HMI可见”关掉。保持区是必须掉电不丢的变量授权激活标志、授权截止日期、历史最大时间戳、试机首次上电时间、非法篡改次数。这些变量必须在DB属性里设置为保持性Retain。如果忘记设保持性客户现场一断电授权状态全丢每次开机都要重新输注册码这体验不用我多说。3. CRC32校验函数与注册码生成器的SCL实现3.1 为什么选CRC32而不是AES或MD5严格从密码学角度看CRC32不是加密算法它是校验算法用做授权码生成在密码学上有先天弱点。但在PLC这个特殊环境里它反而是最务实的方案。原因有三第一S7-1200/1500没有现成的MD5/SHA/AES算法库自己实现AES需要几百行查表代码占用大量DB空间而且在S7-1200上跑得慢第二授权码生成的场景不是网络通信不需要对抗高强度恶意攻击我们需要的是“生成一个够长、够随机、不可批量推导的注册码”第三CRC32的运算量小S7-1200完全跑得动配合加盐、多轮异或、字节反转这些混淆手段即使别人拿到几组注册码也推不出原始密钥。简单说CRC32在这里承担的是“把一串输入变成一串看上去没规律的输出”的散列职责目的不是防密码学攻击而是防止现场的客户或者同行轻松算出注册码。3.2 CRC32逐位运算的SCL实现我直接用逐位运算法不搞查表法。虽然查表法速度快但需要256字的常量表在S7-1200上占DB空间而且调试时想单步看中间结果比较麻烦。逐位法代码量小逻辑直观几十上百字节的数据算一次耗时在毫秒级对授权这种低频操作完全够用。FC代码如下输入是一个Byte数组和有效长度输出CRC32值FUNCTION FC_CRC32_ByteArray : DWord { S7_Optimized_Access : TRUE } VERSION : 0.1 VAR_INPUT aData : Array[0..63] of Byte; // 待计算数据的缓冲区 iLen : Int; // 有效数据长度不能超过64 END_VAR VAR dwCRC : DWord; dwByte : DWord; i : Int; j : Int; END_VAR BEGIN // 长度保护非法长度直接返回0避免数组越界 IF iLen 0 THEN FC_CRC32_ByteArray : 16#00000000; RETURN; END_IF; IF iLen 64 THEN iLen : 64; END_IF; dwCRC : 16#FFFFFFFF; FOR i : 0 TO iLen - 1 DO dwByte : BYTE_TO_DWORD(aData[i]); dwCRC : dwCRC XOR dwByte; FOR j : 1 TO 8 DO IF (dwCRC AND 16#00000001) 16#00000000 THEN dwCRC : (dwCRC SHR 1) XOR 16#EDB88320; ELSE dwCRC : dwCRC SHR 1; END_IF; END_FOR; END_FOR; FC_CRC32_ByteArray : dwCRC XOR 16#FFFFFFFF; END_FUNCTION提示一下不同TIA版本里Byte转DWord的语法略有差异如果编译器提示找不到BYTE_TO_DWORD就改成先转USINT再赋值或者直接写dwByte : aData[i];SCL在部分版本下会做隐式扩展。这种适配在真实工程里很常见别被编译器的报错吓住。3.3 加盐与多轮混淆让注册码不可批量推导裸算CRC32有个问题如果把序列号和CRC结果对照几组理论上可以通过暴力枚举碰撞推出规则。所以必须加盐和混淆。我项目里用的流程是#!/usr/bin/env python3 # 厂家离线注册码生成工具 import zlib def generate_regcode(serial: str, expire_date: str) - str: # 盐值固定写在算法里和PLC侧保持一致 SALT bS7-AUTH-KEY-2024 # 第一步序列号 盐值 授权截止日期 raw serial.upper().encode(ascii) SALT expire_date.encode(ascii) crc1 zlib.crc32(raw) 0xFFFFFFFF # 混淆1异或固定掩码 crc1 ^ 0xA5A5A5A5 # 混淆2字节序反转 crc1 int.from_bytes(crc1.to_bytes(4, little), big) # 第二步用混淆后的值和序列号再算一次 raw2 crc1.to_bytes(4, big) serial.upper().encode(ascii) crc2 zlib.crc32(raw2) 0xFFFFFFFF # 注册码 16位字符串前8位CRC结果后8位截止日期 return f{crc2:08X}{expire_date} if __name__ __main__: serial input(请输入CPU序列号: ).strip() expire input(请输入授权截止日期(YYYYMMDD): ).strip() print(注册码:, generate_regcode(serial, expire))PLC侧校验的逻辑完全对应先取用户输入的注册码字符串截出后8位作为截止日期然后把序列号、盐值、这个截止日期拼起来算CRC再做一次异或和字节反转再算第二次CRC最后把结果转成8位十六进制字符串和注册码前8位比对。这里最容易被忽略的是字节序。Python里int.from_bytes(..., little)和PLC侧DWord的存储顺序要保持一致不然两边算出来的结果永远对不上。我第一次联调时就在这上面卡了一个下午最后把中间值逐个打印出来对比才发现是字节序问题。3.4 注册码比对与防暴力尝试比对注册码时有几个实操细节值得注意不要在PLC侧直接把整个长字符串比较正确做法是算完CRC后比较DWord数值避免大小写、空格等字符串差异。如果实在要比较字符串统一转大写再比。HMI上要加防暴力尝试机制。我见过现场操作工闲着没事乱输注册码输错十几次把CPU搞进STOP的。我的做法是连续输错3次锁定输入框10分钟用保持变量存失败次数和锁定起始时间。第4次再错锁定时间翻倍。这个机制在亚控、西门子精简面板、WinCC上都很好实现。校验通过之后立刻置位“授权激活”保持标志并把截止日期写入保持区。之后每次扫描只需要查这个标志和日期不需要重复跑CRC计算。既省CPU又避免“同一个注册码每次上电都重新算一遍”带来的潜在不一致问题。4. 动态时间锁让设备按授权周期运行并防时钟回拨4.1 读系统时钟RD_LOC_T指令和DTL数据类型时间锁的核心是读取PLC实时时钟。TIA Portal里S7-1200/1500用RD_LOC_T指令读本地时间输出是DTL数据类型的变量。调用方式RD_LOC_T(EN : #bTrigger, RET_VAL : #wRetVal, OUT : #dtlNow);DTL类型是一个结构体里面的YEAR、MONTH、DAY、HOUR、MINUTE、SECOND字段可以直接访问。我习惯把日期转成一个整数再比较#iToday : #dtlNow.YEAR * 10000 #dtlNow.MONTH * 100 #dtlNow.DAY;这样20250115就是一个整数跟授权截止日期比大小非常直观。4.2 授权截止日期的两种判断方式第一种方式精确到天的简单判断。把截止日期存成YYYYMMDD格式的DInt每次上电和每天第一次运行OB时用当前日期和它比对。当前日期大于截止日期就切换为“授权过期”状态。这种方式代码量小完全够用。第二种方式精确到分钟。把DTL转成“从2000年1月1日0点开始的分钟数”这需要自己写一个公历日期换算函数处理闰年和每月天数。代码会翻一倍但在按小时收费的设备租赁场景里值得做。我个人建议绝大多数设备锁机场景用第一种就够了真需要精确到分钟的再上第二种。别一开始就整复杂现场维护的人会骂娘。4.3 时钟回拨检测用历史最大时间戳防篡改只做“当前日期和截止日期比大小”有一个致命漏洞现场的人把PLC时钟改回2020年设备就永远不过期了。所以必须增加一个“历史最大时间戳”检测。具体实现在保持区定义一个变量hMaxDate存放设备运行至今见过的最大日期。每次做时间检查时如果当前日期大于hMaxDate就更新它为当前日期如果当前日期小于hMaxDate说明时钟被人往回拨了立即把授权状态切换为“非法篡改”锁机并记录篡改次数。这个逻辑相当于给时间锁装了一个“只进不退”的棘轮只要拨回去一次就会被发现。但这里有个连锁坑如果S7-1200断电超过超级电容的维持时间RTC会回到出厂默认值比如2000年1月1日。这时当前日期远小于hMaxDate会被误判为非法篡改。所以启用时间锁的项目设备说明书或者现场标识一定要写清楚长期断电超过两周需要重新上电或者干脆配一个不间断电源给PLC保持供电。如果是S7-1500优先选带电池的缓冲模块这个钱不能省。另外防止别人把整个保持区DB初始化来绕过时间锁的招法如果授权激活标志被清零我建议把设备行为设定为“需要重新激活”而不是自动恢复未授权试机状态。这样即使对方操作失误或者故意清DB设备也不会悄悄变成可免费试用的状态至少会暴露问题。5. 叠加专有技术保护从“防看代码”升级到“防复制”5.1 专有技术保护的正确设置动态加密功能块解决的是“程序换个PLC还能跑”的问题但它本身也是代码如果不做代码保护别人上载项目之后直接看SCL你的盐值、算法、密钥就全暴露了。所以第一步还是得叠上博途的专有技术保护。设置方法很简单在项目树里右键你要保护的程序块FB、FC、DB都支持选择属性在“保护”选项卡里勾选“专有技术保护”设置密码。然后重新编译勾选“支持专有技术保护”。编译下载后别人上载这个块只能看到接口定义看不到内部实现。这里有几个操作层面的提醒设置了专有技术保护的DB块同样会被隐藏内容所以不要把授权盐值这种敏感信息放在一个完全公开的DB里最好放在FB的静态变量里随FB一起保护。密码一定要找安全的地方存着最好由两个人分开保管。忘了密码之后正规途径无法找回只能把块删了重写。块的保护级别是“防君子不防小人”网上确实有工具能处理掉专有技术保护所以它只能当第一道门。5.2 双保险组合专有技术保护加动态加密专有技术保护负责“代码不可见”动态加密负责“代码不可异地运行”两者叠加才是完整方案。我的做法是核心工艺FB全部加专有技术保护动态加密授权FB作为独立块输出一个bAuthOK位给主程序主程序里所有关键工艺段的使用使能都引用这个bAuthOK位。一旦授权校验失败bAuthOK为0设备关键动作全部禁止只有面板灯和HMI提示还活着。注意一个细节不要在程序里只用一个M点或者D点做授权标志因为别人上载程序后监控变量直接把那个点的状态改成1就能绕过校验。我的做法是授权状态用枚举值加多个位组合表示比如bAuthOK必须为1同时bLockFlag必须为0同时bTampered必须为0三个条件在多个位置交叉检查。虽说不算真正的防逆转但至少让“找点改值”没那么容易。5.3 关键OB分散校验防止启动逻辑被跳过还有一个容易被忽视的攻击路径很多人把授权初始化放在OB100里OB100是启动组织块只在CPU启动时执行一次。如果别人把OB100的调用删了或者整个项目下载时故意不加载OB100那么授权初始化逻辑就不跑了设备可能直接进入一种“未初始化”状态。针对这个我设计了一套“心跳互检”机制OB100里调用FB_AuthInit做的事情是读取序列号、算CRC、校验注册码、把心跳标志H1置1、记录本次上电时间。OB1每个扫描周期里调用FB_AuthCheck做的事情是检查H1是否为1如果H1为0说明OB100没跑直接判非法篡改同时检查当前时间是否在授权期内然后置心跳标志H2。OB10定时中断里调用FB_AuthLoop每1000毫秒检查一次H1和H2是否都为1只要发现心跳断链就把授权状态改成锁定。三层互相看着谁被删除或者跳过另外两层马上能发现。虽然不是绝对安全但破解成本又高了一截。对付现场客户和一般同行已经足够了。6. 项目实测踩坑记录四个最容易翻车的地方6.1 坑一字符串长度和类型转换最容易把CPU搞进STOPS7-1200默认的String类型是256字节这个长度对注册码来说太大了而且HMI变量连接时长度不一致会导致字符串被截断或者乱码。我的建议是用受限字符串比如String[16]同时在功能块接口定义里严格规定长度。另一个高频崩溃点是SCL里处理String和Byte数组的转换。我的经验是不要在FB内部反复截取字符串而是在HMI侧就把序列号和注册码都准备好PLC侧只做一次字符串到字节缓冲区的转换然后交给CRC函数。转换时特别注意循环边界数组越界在S7-1200上不是报警是直接STOP。生产设备在运行中CPU突然STOP这个后果想想就头疼。安全写法是先判断长度再做循环。我在CRC函数开头已经加了长度保护这个习惯请务必保留。6.2 坑二断电后RTC丢失时间锁瞬间失效这个前面提到过但值得单独拿出来说因为太容易踩了。S7-1200断电后靠超级电容维持RTC一般能撑十几天到二十天。如果设备停机超过这个时间再上电RTC会回到出厂默认时间比如2000年1月1日。后果有两个方向如果做了时钟回拨检测设备会误报非法篡改锁机如果没做设备可能因为“当前日期早于截止日期”而继续运行等于时间锁白做了。我处理这个问题的思路是分两步。第一步在程序里判断如果读取到的时间年份小于2020年且授权激活标志为1就判定为“RTC异常”设备进入受限模式允许维持基本功能运行但核心工艺功能锁定。第二步和设备说明书配合规定长期停机的设备每隔两周必须上电一次。如果实在做不到定期上电那就换S7-1500加电池模块。另外现场调试时有一个低级的坑新出厂的S7-1200RTC时间可能是错的必须先校准时间再激活授权不然注册码里附带的截止日期和实际日期对不上。我第一次做样机测试时就被坑过后来习惯性在设备出厂前统一校准PLC时钟。6.3 坑三优化访问DB的保持性设置细节多到防不胜防TIA Portal从V13开始推荐使用优化访问DB它对符号名和数据结构做了优化但也带来一些新问题。保持性设置上优化DB是在变量属性里单独勾选“保持性”而不是像传统DB那样在DB级别设置。我踩过的坑是有些数据类型在保持性上有长度限制比如超长的String或者大数据数组在部分CPU型号上配置保持性会导致编译不通过。解决办法是把需要保持的数据拆成多个小变量或者改用Array[0..n] of Byte这种基础类型。还有一点下载程序时如果勾选了“初始化保持性存储器”所有保持变量都会被清掉。这个操作往往发生在调试阶段一不小心的操作。有一次我在现场调程序顺手点了初始化客户设备授权状态被清了后来重新输注册码才恢复。从那以后我在授权保持机制里加了一个“授权初始化标记”一旦发现保持区被清空HMI就会弹出一个明确的提示而不是让设备在未知状态下裸奔。6.4 反破解边界动态加密能做什么做不到什么写到最后必须说几句实在话。PLC端的动态加密没有任何一种方案能做到绝对安全。博途的专有技术保护可以被专业工具移除SCL逻辑上载后可以被分析注册码在HMI和PLC通信的变量区里可以被监控抓包甚至有些人不看程序直接把授权判断结果改掉。所以我一直跟客户强调动态加密功能块的目标不是“绝对无法破解”而是“破解成本高于重新开发一套设备方案的成本”。让想抄的人觉得麻烦、让想赖账的人觉得不划算这套程序就值回票价了。商业层面的防线可能比程序更有效合同里约定破解和绕过授权的违约责任、法律层面的知识产权保护、以及提供比破解更省心的正规售后授权服务。程序给技术兜底合同给商业兜底两边同时上锁设备商的权益才能真正立得住。最后再分享一个体会这套动态加密方案我最自豪的不是CRC和状态机设计得多巧妙而是它让设备商在面对“客户拖着尾款不付”的时候多了一个理直气壮又不用撕破脸的谈判筹码。技术解决不了所有商业问题但能给商业问题多一个优雅的解法。
返回列表