態(tài)加密授權(quán)功能塊:設(shè)計(jì)思路與SCL實(shí)現(xiàn)全解析)
這兩年接過(guò)不少西門(mén)子S7-1200/1500的項(xiàng)目十有八九的客戶都會(huì)提同一個(gè)需求能不能做一套動(dòng)態(tài)加密功能塊程序讓設(shè)備只能在指定的PLC上跑授權(quán)到期自動(dòng)鎖機(jī)程序被拷走也跑不起來(lái)。這個(gè)需求聽(tīng)起來(lái)玄乎拆開(kāi)看其實(shí)就是一套自研的授權(quán)與校驗(yàn)邏輯。這篇文章我就把這個(gè)功能塊從設(shè)計(jì)思路、SCL代碼到踩過(guò)的坑完整抖出來(lái)想給自家設(shè)備加“防盜鎖”的按這個(gè)路子走基本能落地。1. 先拆概念S7-1200/1500動(dòng)態(tài)加密功能塊到底在加密什么1.1 “動(dòng)態(tài)加密”不是西門(mén)子標(biāo)準(zhǔn)庫(kù)而是一類(lèi)自研授權(quán)邏輯很多人一聽(tīng)到“動(dòng)態(tài)加密”四個(gè)字以為博途里有個(gè)現(xiàn)成的加密功能塊拖進(jìn)來(lái)設(shè)置個(gè)密碼就能用。這里必須先澄清一個(gè)認(rèn)知西門(mén)子官方并沒(méi)有提供名為“Dynamic Encryption”的標(biāo)準(zhǔn)功能塊。博途自帶的塊保護(hù)叫“專(zhuān)有技術(shù)保護(hù)”Know-how protection它解決的是“上載后別人能不能看到你的梯形圖/SCL代碼”的問(wèn)題本質(zhì)是靜態(tài)密碼保護(hù)。而行業(yè)里常說(shuō)的“動(dòng)態(tài)加密功能塊程序”是工程師基于SCL/STL自己寫(xiě)的一套授權(quán)校驗(yàn)邏輯典型包含設(shè)備唯一標(biāo)識(shí)采集通常是CPU序列號(hào)、注冊(cè)碼/解鎖碼的動(dòng)態(tài)生成與比對(duì)、運(yùn)行許可控制三大部分。之所以叫“動(dòng)態(tài)”是因?yàn)槊看涡r?yàn)的輸入不是寫(xiě)死在程序里的固定常量而是CPU序列號(hào)、當(dāng)前時(shí)間、運(yùn)行次數(shù)這類(lèi)會(huì)變化的變量。同樣的注冊(cè)碼換一臺(tái)PLC、過(guò)了授權(quán)期限統(tǒng)統(tǒng)失效。所以接下來(lái)所有內(nèi)容都是圍繞如何手寫(xiě)這套自研邏輯展開(kāi)的。搞清楚這個(gè)前提你才不會(huì)被網(wǎng)上那些玄乎的“加密神器”帶偏。1.2 三個(gè)典型業(yè)務(wù)場(chǎng)景鎖機(jī)、收尾款、防抄方案我接觸過(guò)的項(xiàng)目里動(dòng)態(tài)加密功能塊基本都在解決三種現(xiàn)實(shí)問(wèn)題第一個(gè)是設(shè)備分期付款??蛻糁桓读耸赘对O(shè)備先拉去用但到了約定時(shí)間點(diǎn)設(shè)備自動(dòng)鎖定想繼續(xù)用就得聯(lián)系廠家付清尾款換解鎖碼。很多做非標(biāo)設(shè)備的兄弟應(yīng)該深有體會(huì)設(shè)備發(fā)出去收不回錢(qián)的情況太常見(jiàn)了機(jī)械鎖容易被拆程序鎖相對(duì)更體面。第二個(gè)是方案防抄。整套設(shè)備程序里最值錢(qián)的是工藝參數(shù)和核心邏輯你用專(zhuān)有技術(shù)保護(hù)把代碼藏起來(lái)之后別人確實(shí)看不到了但人家可以把整個(gè)項(xiàng)目上載下載到另一臺(tái)同型號(hào)PLC里照跑不誤。動(dòng)態(tài)加密把程序“綁定”到特定CPU的序列號(hào)上換一臺(tái)PLC跑不起來(lái)才能攔住這種整體搬運(yùn)。第三個(gè)是臨時(shí)授權(quán)。試機(jī)、借用、展會(huì)演示的設(shè)備給一個(gè)固定使用期限到期自動(dòng)失效。省得派人去現(xiàn)場(chǎng)“拆機(jī)”也能防止客戶把試用設(shè)備當(dāng)正式設(shè)備一直拖下去。1.3 S7-1200和S7-1500的能力邊界決定方案怎么選實(shí)話實(shí)說(shuō)S7-1200和S7-1500都能做動(dòng)態(tài)加密但能力邊界差異明顯方案選型必須提前想清楚。從指令集看S7-1200沒(méi)有官方AES/DES這類(lèi)對(duì)稱(chēng)加密庫(kù)S7-1500同樣沒(méi)有現(xiàn)成的加密指令所以常規(guī)做法是自研CRC32或者XXTEA這類(lèi)輕量級(jí)算法。S7-1500的字符串處理和數(shù)組運(yùn)算性能更強(qiáng)可以做更復(fù)雜的多輪混淆而S7-1200的CPU資源有限算法規(guī)模要控制尤其不能把大數(shù)組查表這種操作放在每個(gè)掃描周期里跑。從讀取設(shè)備標(biāo)識(shí)看S7-1200從V4.0固件開(kāi)始可以用Get_IM_Data指令讀IM0數(shù)據(jù)里面帶CPU序列號(hào)S7-1500同樣支持讀取速度和數(shù)據(jù)結(jié)構(gòu)完整度都更好。如果項(xiàng)目里用的是很老的S7-1200固件版本得先確認(rèn)是否有這個(gè)指令。從實(shí)時(shí)時(shí)鐘看S7-1200斷電后靠超級(jí)電容維持RTC一般能撐十幾天到二十天時(shí)間太長(zhǎng)會(huì)丟S7-1500加電池模塊或緩沖模塊會(huì)更可靠。這一點(diǎn)直接決定“時(shí)間鎖”方案敢不敢用。后面第4節(jié)我會(huì)專(zhuān)門(mén)講這個(gè)坑。還有一個(gè)容易被忽略的點(diǎn)掃描周期。CRC32加時(shí)間比對(duì)如果每個(gè)掃描周期都跑一遍S7-1200的掃描周期會(huì)被明顯拉長(zhǎng)。所以動(dòng)態(tài)加密功能塊絕不應(yīng)該放在OB1的每個(gè)循環(huán)里全程執(zhí)行要么放到定時(shí)中斷OB里節(jié)流執(zhí)行要么在程序里加一個(gè)“每N秒執(zhí)行一次”的判斷。2. 動(dòng)態(tài)授權(quán)FB總體設(shè)計(jì)三層結(jié)構(gòu)和一整套授權(quán)狀態(tài)機(jī)2.1 三層架構(gòu)設(shè)備身份層、授權(quán)校驗(yàn)層、運(yùn)行控制層動(dòng)手寫(xiě)代碼之前先把架構(gòu)理清楚。我建議把整個(gè)動(dòng)態(tài)加密功能塊拆成三層各管各的事調(diào)試的時(shí)候能少掉一半頭發(fā)。第一層是設(shè)備身份層。它的職責(zé)是拿到“這臺(tái)PLC是誰(shuí)”的證據(jù)。最通用的做法是用Get_IM_Data讀取CPU序列號(hào)如果現(xiàn)場(chǎng)條件不允許讀序列號(hào)也可以用廠家自己寫(xiě)入保持區(qū)的一個(gè)設(shè)備編號(hào)。前者好處是換PLC自動(dòng)失效防挪機(jī)后者好處是邏輯簡(jiǎn)單但防不了別人把整個(gè)項(xiàng)目連DB一起搬走。我的經(jīng)驗(yàn)是優(yōu)先用CPU序列號(hào)。第二層是授權(quán)校驗(yàn)層。廠家的離線授權(quán)工具拿到序列號(hào)和授權(quán)截止日期之后用特定算法生成注冊(cè)碼PLC內(nèi)的功能塊用同樣的算法算一遍兩者比對(duì)一致才放行。這一層的關(guān)鍵是算法一致性后面第3節(jié)詳細(xì)寫(xiě)。第三層是運(yùn)行控制層。校驗(yàn)結(jié)果不是一個(gè)簡(jiǎn)單的布爾量就完了它要管理一套授權(quán)狀態(tài)機(jī)分別處理未授權(quán)、已授權(quán)、試用期、授權(quán)過(guò)期、非法篡改等狀態(tài)。不同狀態(tài)對(duì)應(yīng)不同動(dòng)作未授權(quán)可以跑30分鐘試機(jī)授權(quán)過(guò)期每小時(shí)停機(jī)5分鐘并亮黃燈非法篡改直接鎖機(jī)并記錄次數(shù)。狀態(tài)機(jī)的好處是把邏輯邊界劃清楚不會(huì)出現(xiàn)“到底要不要停機(jī)”這種模糊狀態(tài)。2.2 授權(quán)狀態(tài)機(jī)的設(shè)計(jì)細(xì)節(jié)狀態(tài)機(jī)的狀態(tài)定義我一般這樣枚舉狀態(tài)含義觸發(fā)條件設(shè)備行為0 未授權(quán)從未輸入過(guò)有效注冊(cè)碼上電且授權(quán)標(biāo)志位為0允許試機(jī)N小時(shí)HMI提示輸入注冊(cè)碼1 已授權(quán)注冊(cè)碼校驗(yàn)通過(guò)且在有效期內(nèi)校驗(yàn)通過(guò)且當(dāng)前日期截止日期全功能開(kāi)放2 試用期沒(méi)有正式授權(quán)但處于首次試機(jī)時(shí)間內(nèi)首次上電時(shí)間試用時(shí)長(zhǎng)當(dāng)前時(shí)間全功能開(kāi)放HMI顯示剩余試用時(shí)間3 授權(quán)過(guò)期超過(guò)截止日期當(dāng)前日期截止日期且曾經(jīng)授權(quán)過(guò)鎖定工藝功能每小時(shí)解鎖5分鐘4 非法篡改檢測(cè)到時(shí)鐘回?fù)芑驍?shù)據(jù)異常歷史最大時(shí)間戳當(dāng)前時(shí)間立即鎖機(jī)需廠家遠(yuǎn)程解鎖碼恢復(fù)5 永久鎖定連續(xù)多次非法篡改非法篡改次數(shù)3永久鎖機(jī)只能返廠處理狀態(tài)轉(zhuǎn)換的邏輯集中在授權(quán)FB內(nèi)部輸出一個(gè)枚舉或整數(shù)型狀態(tài)值給HMI顯示。設(shè)備的行為動(dòng)作則由主程序根據(jù)這個(gè)狀態(tài)值去控制不要在FB內(nèi)部直接寫(xiě)一堆M輸出那樣復(fù)用性會(huì)很差。2.3 授權(quán)數(shù)據(jù)塊的設(shè)計(jì)原則公開(kāi)區(qū)、隱藏區(qū)、保持區(qū)授權(quán)FB配套的數(shù)據(jù)塊是整個(gè)方案的命脈布局我習(xí)慣分三個(gè)區(qū)公開(kāi)區(qū)給HMI讀寫(xiě)包括輸入序列號(hào)顯示或者自動(dòng)讀取后的顯示、輸入注冊(cè)碼的變量、授權(quán)狀態(tài)值、剩余天數(shù)、剩余試機(jī)時(shí)間。這些變量HMI能讀能寫(xiě)方便現(xiàn)場(chǎng)人員操作。隱藏區(qū)是FB靜態(tài)變量或者私有DB包括鹽值數(shù)組、歷史最大時(shí)間戳、失敗嘗試次數(shù)、鎖定標(biāo)志、授權(quán)校驗(yàn)通過(guò)標(biāo)志。這些變量只允許程序內(nèi)部訪問(wèn)HMI不需要看到。在博途里如果用的是一般DB且沒(méi)有勾選“從HMI可見(jiàn)”HMI就訪問(wèn)不到如果用的是優(yōu)化訪問(wèn)DB注意在變量屬性里把“HMI可見(jiàn)”關(guān)掉。保持區(qū)是必須掉電不丟的變量授權(quán)激活標(biāo)志、授權(quán)截止日期、歷史最大時(shí)間戳、試機(jī)首次上電時(shí)間、非法篡改次數(shù)。這些變量必須在DB屬性里設(shè)置為保持性Retain。如果忘記設(shè)保持性客戶現(xiàn)場(chǎng)一斷電授權(quán)狀態(tài)全丟每次開(kāi)機(jī)都要重新輸注冊(cè)碼這體驗(yàn)不用我多說(shuō)。3. CRC32校驗(yàn)函數(shù)與注冊(cè)碼生成器的SCL實(shí)現(xiàn)3.1 為什么選CRC32而不是AES或MD5嚴(yán)格從密碼學(xué)角度看CRC32不是加密算法它是校驗(yàn)算法用做授權(quán)碼生成在密碼學(xué)上有先天弱點(diǎn)。但在PLC這個(gè)特殊環(huán)境里它反而是最務(wù)實(shí)的方案。原因有三第一S7-1200/1500沒(méi)有現(xiàn)成的MD5/SHA/AES算法庫(kù)自己實(shí)現(xiàn)AES需要幾百行查表代碼占用大量DB空間而且在S7-1200上跑得慢第二授權(quán)碼生成的場(chǎng)景不是網(wǎng)絡(luò)通信不需要對(duì)抗高強(qiáng)度惡意攻擊我們需要的是“生成一個(gè)夠長(zhǎng)、夠隨機(jī)、不可批量推導(dǎo)的注冊(cè)碼”第三CRC32的運(yùn)算量小S7-1200完全跑得動(dòng)配合加鹽、多輪異或、字節(jié)反轉(zhuǎn)這些混淆手段即使別人拿到幾組注冊(cè)碼也推不出原始密鑰。簡(jiǎn)單說(shuō)CRC32在這里承擔(dān)的是“把一串輸入變成一串看上去沒(méi)規(guī)律的輸出”的散列職責(zé)目的不是防密碼學(xué)攻擊而是防止現(xiàn)場(chǎng)的客戶或者同行輕松算出注冊(cè)碼。3.2 CRC32逐位運(yùn)算的SCL實(shí)現(xiàn)我直接用逐位運(yùn)算法不搞查表法。雖然查表法速度快但需要256字的常量表在S7-1200上占DB空間而且調(diào)試時(shí)想單步看中間結(jié)果比較麻煩。逐位法代碼量小邏輯直觀幾十上百字節(jié)的數(shù)據(jù)算一次耗時(shí)在毫秒級(jí)對(duì)授權(quán)這種低頻操作完全夠用。FC代碼如下輸入是一個(gè)Byte數(shù)組和有效長(zhǎng)度輸出CRC32值FUNCTION FC_CRC32_ByteArray : DWord { S7_Optimized_Access : TRUE } VERSION : 0.1 VAR_INPUT aData : Array[0..63] of Byte; // 待計(jì)算數(shù)據(jù)的緩沖區(qū) iLen : Int; // 有效數(shù)據(jù)長(zhǎng)度不能超過(guò)64 END_VAR VAR dwCRC : DWord; dwByte : DWord; i : Int; j : Int; END_VAR BEGIN // 長(zhǎng)度保護(hù)非法長(zhǎng)度直接返回0避免數(shù)組越界 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轉(zhuǎn)DWord的語(yǔ)法略有差異如果編譯器提示找不到BYTE_TO_DWORD就改成先轉(zhuǎn)USINT再賦值或者直接寫(xiě)dwByte : aData[i];SCL在部分版本下會(huì)做隱式擴(kuò)展。這種適配在真實(shí)工程里很常見(jiàn)別被編譯器的報(bào)錯(cuò)嚇住。3.3 加鹽與多輪混淆讓注冊(cè)碼不可批量推導(dǎo)裸算CRC32有個(gè)問(wèn)題如果把序列號(hào)和CRC結(jié)果對(duì)照幾組理論上可以通過(guò)暴力枚舉碰撞推出規(guī)則。所以必須加鹽和混淆。我項(xiàng)目里用的流程是#!/usr/bin/env python3 # 廠家離線注冊(cè)碼生成工具 import zlib def generate_regcode(serial: str, expire_date: str) - str: # 鹽值固定寫(xiě)在算法里和PLC側(cè)保持一致 SALT bS7-AUTH-KEY-2024 # 第一步序列號(hào) 鹽值 授權(quán)截止日期 raw serial.upper().encode(ascii) SALT expire_date.encode(ascii) crc1 zlib.crc32(raw) 0xFFFFFFFF # 混淆1異或固定掩碼 crc1 ^ 0xA5A5A5A5 # 混淆2字節(jié)序反轉(zhuǎn) crc1 int.from_bytes(crc1.to_bytes(4, little), big) # 第二步用混淆后的值和序列號(hào)再算一次 raw2 crc1.to_bytes(4, big) serial.upper().encode(ascii) crc2 zlib.crc32(raw2) 0xFFFFFFFF # 注冊(cè)碼 16位字符串前8位CRC結(jié)果后8位截止日期 return f{crc2:08X}{expire_date} if __name__ __main__: serial input(請(qǐng)輸入CPU序列號(hào): ).strip() expire input(請(qǐng)輸入授權(quán)截止日期(YYYYMMDD): ).strip() print(注冊(cè)碼:, generate_regcode(serial, expire))PLC側(cè)校驗(yàn)的邏輯完全對(duì)應(yīng)先取用戶輸入的注冊(cè)碼字符串截出后8位作為截止日期然后把序列號(hào)、鹽值、這個(gè)截止日期拼起來(lái)算CRC再做一次異或和字節(jié)反轉(zhuǎn)再算第二次CRC最后把結(jié)果轉(zhuǎn)成8位十六進(jìn)制字符串和注冊(cè)碼前8位比對(duì)。這里最容易被忽略的是字節(jié)序。Python里int.from_bytes(..., little)和PLC側(cè)DWord的存儲(chǔ)順序要保持一致不然兩邊算出來(lái)的結(jié)果永遠(yuǎn)對(duì)不上。我第一次聯(lián)調(diào)時(shí)就在這上面卡了一個(gè)下午最后把中間值逐個(gè)打印出來(lái)對(duì)比才發(fā)現(xiàn)是字節(jié)序問(wèn)題。3.4 注冊(cè)碼比對(duì)與防暴力嘗試比對(duì)注冊(cè)碼時(shí)有幾個(gè)實(shí)操細(xì)節(jié)值得注意不要在PLC側(cè)直接把整個(gè)長(zhǎng)字符串比較正確做法是算完CRC后比較DWord數(shù)值避免大小寫(xiě)、空格等字符串差異。如果實(shí)在要比較字符串統(tǒng)一轉(zhuǎn)大寫(xiě)再比。HMI上要加防暴力嘗試機(jī)制。我見(jiàn)過(guò)現(xiàn)場(chǎng)操作工閑著沒(méi)事亂輸注冊(cè)碼輸錯(cuò)十幾次把CPU搞進(jìn)STOP的。我的做法是連續(xù)輸錯(cuò)3次鎖定輸入框10分鐘用保持變量存失敗次數(shù)和鎖定起始時(shí)間。第4次再錯(cuò)鎖定時(shí)間翻倍。這個(gè)機(jī)制在亞控、西門(mén)子精簡(jiǎn)面板、WinCC上都很好實(shí)現(xiàn)。校驗(yàn)通過(guò)之后立刻置位“授權(quán)激活”保持標(biāo)志并把截止日期寫(xiě)入保持區(qū)。之后每次掃描只需要查這個(gè)標(biāo)志和日期不需要重復(fù)跑CRC計(jì)算。既省CPU又避免“同一個(gè)注冊(cè)碼每次上電都重新算一遍”帶來(lái)的潛在不一致問(wèn)題。4. 動(dòng)態(tài)時(shí)間鎖讓設(shè)備按授權(quán)周期運(yùn)行并防時(shí)鐘回?fù)?.1 讀系統(tǒng)時(shí)鐘RD_LOC_T指令和DTL數(shù)據(jù)類(lèi)型時(shí)間鎖的核心是讀取PLC實(shí)時(shí)時(shí)鐘。TIA Portal里S7-1200/1500用RD_LOC_T指令讀本地時(shí)間輸出是DTL數(shù)據(jù)類(lèi)型的變量。調(diào)用方式RD_LOC_T(EN : #bTrigger, RET_VAL : #wRetVal, OUT : #dtlNow);DTL類(lèi)型是一個(gè)結(jié)構(gòu)體里面的YEAR、MONTH、DAY、HOUR、MINUTE、SECOND字段可以直接訪問(wèn)。我習(xí)慣把日期轉(zhuǎn)成一個(gè)整數(shù)再比較#iToday : #dtlNow.YEAR * 10000 #dtlNow.MONTH * 100 #dtlNow.DAY;這樣20250115就是一個(gè)整數(shù)跟授權(quán)截止日期比大小非常直觀。4.2 授權(quán)截止日期的兩種判斷方式第一種方式精確到天的簡(jiǎn)單判斷。把截止日期存成YYYYMMDD格式的DInt每次上電和每天第一次運(yùn)行OB時(shí)用當(dāng)前日期和它比對(duì)。當(dāng)前日期大于截止日期就切換為“授權(quán)過(guò)期”狀態(tài)。這種方式代碼量小完全夠用。第二種方式精確到分鐘。把DTL轉(zhuǎn)成“從2000年1月1日0點(diǎn)開(kāi)始的分鐘數(shù)”這需要自己寫(xiě)一個(gè)公歷日期換算函數(shù)處理閏年和每月天數(shù)。代碼會(huì)翻一倍但在按小時(shí)收費(fèi)的設(shè)備租賃場(chǎng)景里值得做。我個(gè)人建議絕大多數(shù)設(shè)備鎖機(jī)場(chǎng)景用第一種就夠了真需要精確到分鐘的再上第二種。別一開(kāi)始就整復(fù)雜現(xiàn)場(chǎng)維護(hù)的人會(huì)罵娘。4.3 時(shí)鐘回?fù)軝z測(cè)用歷史最大時(shí)間戳防篡改只做“當(dāng)前日期和截止日期比大小”有一個(gè)致命漏洞現(xiàn)場(chǎng)的人把PLC時(shí)鐘改回2020年設(shè)備就永遠(yuǎn)不過(guò)期了。所以必須增加一個(gè)“歷史最大時(shí)間戳”檢測(cè)。具體實(shí)現(xiàn)在保持區(qū)定義一個(gè)變量hMaxDate存放設(shè)備運(yùn)行至今見(jiàn)過(guò)的最大日期。每次做時(shí)間檢查時(shí)如果當(dāng)前日期大于hMaxDate就更新它為當(dāng)前日期如果當(dāng)前日期小于hMaxDate說(shuō)明時(shí)鐘被人往回?fù)芰肆⒓窗咽跈?quán)狀態(tài)切換為“非法篡改”鎖機(jī)并記錄篡改次數(shù)。這個(gè)邏輯相當(dāng)于給時(shí)間鎖裝了一個(gè)“只進(jìn)不退”的棘輪只要撥回去一次就會(huì)被發(fā)現(xiàn)。但這里有個(gè)連鎖坑如果S7-1200斷電超過(guò)超級(jí)電容的維持時(shí)間RTC會(huì)回到出廠默認(rèn)值比如2000年1月1日。這時(shí)當(dāng)前日期遠(yuǎn)小于hMaxDate會(huì)被誤判為非法篡改。所以啟用時(shí)間鎖的項(xiàng)目設(shè)備說(shuō)明書(shū)或者現(xiàn)場(chǎng)標(biāo)識(shí)一定要寫(xiě)清楚長(zhǎng)期斷電超過(guò)兩周需要重新上電或者干脆配一個(gè)不間斷電源給PLC保持供電。如果是S7-1500優(yōu)先選帶電池的緩沖模塊這個(gè)錢(qián)不能省。另外防止別人把整個(gè)保持區(qū)DB初始化來(lái)繞過(guò)時(shí)間鎖的招法如果授權(quán)激活標(biāo)志被清零我建議把設(shè)備行為設(shè)定為“需要重新激活”而不是自動(dòng)恢復(fù)未授權(quán)試機(jī)狀態(tài)。這樣即使對(duì)方操作失誤或者故意清DB設(shè)備也不會(huì)悄悄變成可免費(fèi)試用的狀態(tài)至少會(huì)暴露問(wèn)題。5. 疊加專(zhuān)有技術(shù)保護(hù)從“防看代碼”升級(jí)到“防復(fù)制”5.1 專(zhuān)有技術(shù)保護(hù)的正確設(shè)置動(dòng)態(tài)加密功能塊解決的是“程序換個(gè)PLC還能跑”的問(wèn)題但它本身也是代碼如果不做代碼保護(hù)別人上載項(xiàng)目之后直接看SCL你的鹽值、算法、密鑰就全暴露了。所以第一步還是得疊上博途的專(zhuān)有技術(shù)保護(hù)。設(shè)置方法很簡(jiǎn)單在項(xiàng)目樹(shù)里右鍵你要保護(hù)的程序塊FB、FC、DB都支持選擇屬性在“保護(hù)”選項(xiàng)卡里勾選“專(zhuān)有技術(shù)保護(hù)”設(shè)置密碼。然后重新編譯勾選“支持專(zhuān)有技術(shù)保護(hù)”。編譯下載后別人上載這個(gè)塊只能看到接口定義看不到內(nèi)部實(shí)現(xiàn)。這里有幾個(gè)操作層面的提醒設(shè)置了專(zhuān)有技術(shù)保護(hù)的DB塊同樣會(huì)被隱藏內(nèi)容所以不要把授權(quán)鹽值這種敏感信息放在一個(gè)完全公開(kāi)的DB里最好放在FB的靜態(tài)變量里隨FB一起保護(hù)。密碼一定要找安全的地方存著最好由兩個(gè)人分開(kāi)保管。忘了密碼之后正規(guī)途徑無(wú)法找回只能把塊刪了重寫(xiě)。塊的保護(hù)級(jí)別是“防君子不防小人”網(wǎng)上確實(shí)有工具能處理掉專(zhuān)有技術(shù)保護(hù)所以它只能當(dāng)?shù)谝坏篱T(mén)。5.2 雙保險(xiǎn)組合專(zhuān)有技術(shù)保護(hù)加動(dòng)態(tài)加密專(zhuān)有技術(shù)保護(hù)負(fù)責(zé)“代碼不可見(jiàn)”動(dòng)態(tài)加密負(fù)責(zé)“代碼不可異地運(yùn)行”兩者疊加才是完整方案。我的做法是核心工藝FB全部加專(zhuān)有技術(shù)保護(hù)動(dòng)態(tài)加密授權(quán)FB作為獨(dú)立塊輸出一個(gè)bAuthOK位給主程序主程序里所有關(guān)鍵工藝段的使用使能都引用這個(gè)bAuthOK位。一旦授權(quán)校驗(yàn)失敗bAuthOK為0設(shè)備關(guān)鍵動(dòng)作全部禁止只有面板燈和HMI提示還活著。注意一個(gè)細(xì)節(jié)不要在程序里只用一個(gè)M點(diǎn)或者D點(diǎn)做授權(quán)標(biāo)志因?yàn)閯e人上載程序后監(jiān)控變量直接把那個(gè)點(diǎn)的狀態(tài)改成1就能繞過(guò)校驗(yàn)。我的做法是授權(quán)狀態(tài)用枚舉值加多個(gè)位組合表示比如bAuthOK必須為1同時(shí)bLockFlag必須為0同時(shí)bTampered必須為0三個(gè)條件在多個(gè)位置交叉檢查。雖說(shuō)不算真正的防逆轉(zhuǎn)但至少讓“找點(diǎn)改值”沒(méi)那么容易。5.3 關(guān)鍵OB分散校驗(yàn)防止啟動(dòng)邏輯被跳過(guò)還有一個(gè)容易被忽視的攻擊路徑很多人把授權(quán)初始化放在OB100里OB100是啟動(dòng)組織塊只在CPU啟動(dòng)時(shí)執(zhí)行一次。如果別人把OB100的調(diào)用刪了或者整個(gè)項(xiàng)目下載時(shí)故意不加載OB100那么授權(quán)初始化邏輯就不跑了設(shè)備可能直接進(jìn)入一種“未初始化”狀態(tài)。針對(duì)這個(gè)我設(shè)計(jì)了一套“心跳互檢”機(jī)制OB100里調(diào)用FB_AuthInit做的事情是讀取序列號(hào)、算CRC、校驗(yàn)注冊(cè)碼、把心跳標(biāo)志H1置1、記錄本次上電時(shí)間。OB1每個(gè)掃描周期里調(diào)用FB_AuthCheck做的事情是檢查H1是否為1如果H1為0說(shuō)明OB100沒(méi)跑直接判非法篡改同時(shí)檢查當(dāng)前時(shí)間是否在授權(quán)期內(nèi)然后置心跳標(biāo)志H2。OB10定時(shí)中斷里調(diào)用FB_AuthLoop每1000毫秒檢查一次H1和H2是否都為1只要發(fā)現(xiàn)心跳斷鏈就把授權(quán)狀態(tài)改成鎖定。三層互相看著誰(shuí)被刪除或者跳過(guò)另外兩層馬上能發(fā)現(xiàn)。雖然不是絕對(duì)安全但破解成本又高了一截。對(duì)付現(xiàn)場(chǎng)客戶和一般同行已經(jīng)足夠了。6. 項(xiàng)目實(shí)測(cè)踩坑記錄四個(gè)最容易翻車(chē)的地方6.1 坑一字符串長(zhǎng)度和類(lèi)型轉(zhuǎn)換最容易把CPU搞進(jìn)STOPS7-1200默認(rèn)的String類(lèi)型是256字節(jié)這個(gè)長(zhǎng)度對(duì)注冊(cè)碼來(lái)說(shuō)太大了而且HMI變量連接時(shí)長(zhǎng)度不一致會(huì)導(dǎo)致字符串被截?cái)嗷蛘邅y碼。我的建議是用受限字符串比如String[16]同時(shí)在功能塊接口定義里嚴(yán)格規(guī)定長(zhǎng)度。另一個(gè)高頻崩潰點(diǎn)是SCL里處理String和Byte數(shù)組的轉(zhuǎn)換。我的經(jīng)驗(yàn)是不要在FB內(nèi)部反復(fù)截取字符串而是在HMI側(cè)就把序列號(hào)和注冊(cè)碼都準(zhǔn)備好PLC側(cè)只做一次字符串到字節(jié)緩沖區(qū)的轉(zhuǎn)換然后交給CRC函數(shù)。轉(zhuǎn)換時(shí)特別注意循環(huán)邊界數(shù)組越界在S7-1200上不是報(bào)警是直接STOP。生產(chǎn)設(shè)備在運(yùn)行中CPU突然STOP這個(gè)后果想想就頭疼。安全寫(xiě)法是先判斷長(zhǎng)度再做循環(huán)。我在CRC函數(shù)開(kāi)頭已經(jīng)加了長(zhǎng)度保護(hù)這個(gè)習(xí)慣請(qǐng)務(wù)必保留。6.2 坑二斷電后RTC丟失時(shí)間鎖瞬間失效這個(gè)前面提到過(guò)但值得單獨(dú)拿出來(lái)說(shuō)因?yàn)樘菀撞攘恕7-1200斷電后靠超級(jí)電容維持RTC一般能撐十幾天到二十天。如果設(shè)備停機(jī)超過(guò)這個(gè)時(shí)間再上電RTC會(huì)回到出廠默認(rèn)時(shí)間比如2000年1月1日。后果有兩個(gè)方向如果做了時(shí)鐘回?fù)軝z測(cè)設(shè)備會(huì)誤報(bào)非法篡改鎖機(jī)如果沒(méi)做設(shè)備可能因?yàn)椤爱?dāng)前日期早于截止日期”而繼續(xù)運(yùn)行等于時(shí)間鎖白做了。我處理這個(gè)問(wèn)題的思路是分兩步。第一步在程序里判斷如果讀取到的時(shí)間年份小于2020年且授權(quán)激活標(biāo)志為1就判定為“RTC異?!痹O(shè)備進(jìn)入受限模式允許維持基本功能運(yùn)行但核心工藝功能鎖定。第二步和設(shè)備說(shuō)明書(shū)配合規(guī)定長(zhǎng)期停機(jī)的設(shè)備每隔兩周必須上電一次。如果實(shí)在做不到定期上電那就換S7-1500加電池模塊。另外現(xiàn)場(chǎng)調(diào)試時(shí)有一個(gè)低級(jí)的坑新出廠的S7-1200RTC時(shí)間可能是錯(cuò)的必須先校準(zhǔn)時(shí)間再激活授權(quán)不然注冊(cè)碼里附帶的截止日期和實(shí)際日期對(duì)不上。我第一次做樣機(jī)測(cè)試時(shí)就被坑過(guò)后來(lái)習(xí)慣性在設(shè)備出廠前統(tǒng)一校準(zhǔn)PLC時(shí)鐘。6.3 坑三優(yōu)化訪問(wèn)DB的保持性設(shè)置細(xì)節(jié)多到防不勝防TIA Portal從V13開(kāi)始推薦使用優(yōu)化訪問(wèn)DB它對(duì)符號(hào)名和數(shù)據(jù)結(jié)構(gòu)做了優(yōu)化但也帶來(lái)一些新問(wèn)題。保持性設(shè)置上優(yōu)化DB是在變量屬性里單獨(dú)勾選“保持性”而不是像傳統(tǒng)DB那樣在DB級(jí)別設(shè)置。我踩過(guò)的坑是有些數(shù)據(jù)類(lèi)型在保持性上有長(zhǎng)度限制比如超長(zhǎng)的String或者大數(shù)據(jù)數(shù)組在部分CPU型號(hào)上配置保持性會(huì)導(dǎo)致編譯不通過(guò)。解決辦法是把需要保持的數(shù)據(jù)拆成多個(gè)小變量或者改用Array[0..n] of Byte這種基礎(chǔ)類(lèi)型。還有一點(diǎn)下載程序時(shí)如果勾選了“初始化保持性存儲(chǔ)器”所有保持變量都會(huì)被清掉。這個(gè)操作往往發(fā)生在調(diào)試階段一不小心的操作。有一次我在現(xiàn)場(chǎng)調(diào)程序順手點(diǎn)了初始化客戶設(shè)備授權(quán)狀態(tài)被清了后來(lái)重新輸注冊(cè)碼才恢復(fù)。從那以后我在授權(quán)保持機(jī)制里加了一個(gè)“授權(quán)初始化標(biāo)記”一旦發(fā)現(xiàn)保持區(qū)被清空HMI就會(huì)彈出一個(gè)明確的提示而不是讓設(shè)備在未知狀態(tài)下裸奔。6.4 反破解邊界動(dòng)態(tài)加密能做什么做不到什么寫(xiě)到最后必須說(shuō)幾句實(shí)在話。PLC端的動(dòng)態(tài)加密沒(méi)有任何一種方案能做到絕對(duì)安全。博途的專(zhuān)有技術(shù)保護(hù)可以被專(zhuān)業(yè)工具移除SCL邏輯上載后可以被分析注冊(cè)碼在HMI和PLC通信的變量區(qū)里可以被監(jiān)控抓包甚至有些人不看程序直接把授權(quán)判斷結(jié)果改掉。所以我一直跟客戶強(qiáng)調(diào)動(dòng)態(tài)加密功能塊的目標(biāo)不是“絕對(duì)無(wú)法破解”而是“破解成本高于重新開(kāi)發(fā)一套設(shè)備方案的成本”。讓想抄的人覺(jué)得麻煩、讓想賴賬的人覺(jué)得不劃算這套程序就值回票價(jià)了。商業(yè)層面的防線可能比程序更有效合同里約定破解和繞過(guò)授權(quán)的違約責(zé)任、法律層面的知識(shí)產(chǎn)權(quán)保護(hù)、以及提供比破解更省心的正規(guī)售后授權(quán)服務(wù)。程序給技術(shù)兜底合同給商業(yè)兜底兩邊同時(shí)上鎖設(shè)備商的權(quán)益才能真正立得住。最后再分享一個(gè)體會(huì)這套動(dòng)態(tài)加密方案我最自豪的不是CRC和狀態(tài)機(jī)設(shè)計(jì)得多巧妙而是它讓設(shè)備商在面對(duì)“客戶拖著尾款不付”的時(shí)候多了一個(gè)理直氣壯又不用撕破臉的談判籌碼。技術(shù)解決不了所有商業(yè)問(wèn)題但能給商業(yè)問(wèn)題多一個(gè)優(yōu)雅的解法。