☰
LabVIEW授权保护:内置时间锁与序列号解锁机制
2026/10/12 7:17:19 网站建设 项目流程

1. 为什么LabVIEW项目交付要内置时间锁与序列号解锁机制

在LabVIEW外包项目里摸爬滚打这些年,我踩过最疼的坑其实不是程序崩溃、通信超时这类技术问题,而是软件交付了、客户也用了,尾款却迟迟到不了账。打电话催,对方说财务流程走完了、明天就转;过两天再问,又变成领导出差没签字。最尴尬的是,程序已经稳稳跑在对方的生产电脑上,而我手里没有任何“停止开关”。某些合作方抓住这一点,把付款周期一拖再拖,项目直接变成薛定谔的尾款。

被拖过两次之后,我意识到一个很朴素的道理:技术上的主动权,必须在交付之前就设计到软件里。于是我开始给所有商业交付项目内置一套授权保护模块——规定试用天数,到期自动锁定;客户需要续期时,把机器码发给我,我生成一个对应序列号,对方输入后按设定方案自动解锁。这套模块后来被我从具体项目里抽出来,做成了一份可以反复使用的工具包,在日后的每个商业项目里都发挥了实实在在的作用。

复盘这套模块要解决的需求,其实非常聚焦:

第一是使用权的时间限制。客户拿到软件后只能试用N天(可配置),超过规定天数后进入锁定状态,功能不可用。第二是序列号自动解锁。锁定界面收集机器码,开发者校验身份后生成序列号,客户输入后立刻恢复全部功能,不需要远程登录对方电脑操作。第三是可配置的授权周期。同一个客户端可以针对不同订单生成不同到期日期,比如三个月的短期授权、半年交付期的中期授权、或者永久授权,都由序列号携带的信息决定。第四是防篡改防破解。至少要做到普通操作者无法通过改系统时间、删除文件绕过去,否则这套机制就只是给客户添麻烦,起不到真正的约束作用。

很多人第一反应是:直接在软件里写死“只运行到某一天”不就行了吗?这确实是最简单的方案,但在真实项目里完全不够。客户机器可能长时间关机,或者中途重做系统;更常见的是测试人员手动把系统日期往后拨,试一下能不能绕过限制。单纯写死日期、只存一个变量,等于把自己暴露在“稍微懂点电脑的人都能绕过去”的尴尬境地。所以我在选型时采用了三个关键设计:机器码绑定、天数差计算、多位置冗余存储。机器码保证一个序列号只能用于一台电脑,天数差保证你能扛住未经授权的日期更改,冗余存储则保证客户无法靠删除一个文件来重置试用期。三者各管一段,合在一起才构成一条完整的授权逻辑链。

这套方案对硬件的要求极低,哪怕客户用的是五年前的工控机,跑LabVIEW编译出来的exe也毫无压力。对开发者来说,唯一多出来的成本是写一个序列号生成小工具,以及几小时串起授权逻辑的时间。但换来的是一旦项目验收拖延、客户失联,你手上永远握着一个礼貌而有效的“暂停键”。

2. 授权模块的状态机设计:试用、锁定、激活三种状态怎么切换

授权保护模块的代码不难,难的是把边界情况想清楚。我最开始写第一版时只考虑了单一路径:首次运行记录时间,超过N天就锁死,输入序列号解锁。结果测试第一天就翻车——客户调了系统时间,模块直接把软件锁了,客户还不明所以,一顿电话解释。从那之后,我改用状态机的方式来组织整个授权逻辑,把每条可能路径都提前画清楚。

2.1 四种核心状态与状态跳转逻辑

整个授权模块只维护四个状态:

状态含义模块行为
TRIAL_INIT首次启动,尚未建立试用记录写首启日期、末次启动日期,进入试用计数
TRIAL_ACTIVE处于试用期内正常执行主程序,更新“末次启动日期”
LOCKED超过试用天数或检测到篡改显示激活窗口,阻止主程序逻辑运行
LICENSED已验证授权信息,激活成功正常执行主程序,不再受试用期限制

状态跳转的关键判定逻辑是这样的:

  • 启动时读取本地存储的授权状态文件(文件读不到,说明是首次运行,进入TRIAL_INIT)。
  • 尝试读取“到期日期”。如果有且未过期,直接进入LICENSED。
  • 没有到期日期,就按试用逻辑走:读取“首次运行日期”和“末次运行日期”,利用当前日期和两者的关系判断是否被篡改,再决定放行还是锁定。
  • 用户输入序列号,模块先校验序列号与机器码是否匹配,再校验序列号携带的到期日是否合法,通过后写入授权信息,状态切换为LICENSED。
  • 授权到期日当天再次启动,状态转回LOCKED,重新要求激活。

这一套状态机写清楚后,后面所有功能模块都在自己的状态内部动作,不会再出现“改了这块影响那块”的连锁问题。

2.2 首启时间的三位置冗余存储方案

“首次运行日期”是整个试用期计数的基准。客户如果把存储位置里的日期改了,或者干脆删掉文件,就相当于把自己重置成了“新用户”。行业内对这种问题的处理通常叫多位置冗余存储,简单说就是同一个信息放三份,互相对照。

我在模块里选择的三个位置分别是:

  • 注册表:HKEY_CURRENT_USER\Software\YourCompany\YourApp\License,保存InstallTime、LastRunTime、ExpireDate、MachineCode;
  • 应用目录隐藏文件:C:\Program Files\YourCompany\YourApp\lic.dat,同样保存上述四项;
  • 用户数据目录:%APPDATA%\YourCompany\YourApp\lic.dat,作为独立副本。

三份数据写入的时间点统一在状态机完成判定之后。读取时以“多数一致”为原则:如果三份数据两两一致,就以该结果为准;如果只有一份被篡改,另外两份一致,就直接用一致的那两份,并且把不一致的那份重新写回正确内容,相当于自动修复;如果三份全部被删除或内容各不相同,才会被判定为“首次运行”,重新进入试用流程。

这个方案我在多个项目里实测下来,普通客户基本没有能力绕过。因为即使对方找到三处文件并全部删除,那也只会触发“重新试用”,第一次安装时间被清除,但客户立刻会意识到试用天数被重置了——这在商业沟通里其实是明显违约信号,大多数懂行的技术人员反而会因此提醒项目经理尽快结清款项。

2.3 天数判定的边界情况:跨月、闰年、时间回拨

天数计算看起来简单:当前日期减去首次运行日期。但真实运行环境里有一堆边界情况。

跨月和闰年交给函数库处理,LabVIEW原生支持UTC时间计算,把两个日期转成秒后相减,再除以86400换算成天数,就自动规避了大小月和闰年问题。

时间回拨才是我重点要防的。客户如果把系统日期改到很久以前,重新运行软件,按照“当前日期减去首启日期”的逻辑,试用期可能变成负数,永远不锁定。应对办法是:额外保存一笔LastRunTime(最近一次成功运行时的系统日期)。每次启动时都检查,如果当前日期 < LastRunTime - 1天,就认为是一次时间回拨,此时不再信任当前日期,而是直接把状态切换成LOCKED并要求重新激活,同时在激活窗口里提示“检测到系统时间异常”。

有人可能会问:为什么留了一天容差?因为客户机器有可能被系统自动校时,或者时区设置变化导致日期出现小范围跳动。留一天容差可以避免正常使用者的误伤,又能挡住绝大多数手动修改系统时间的绕过尝试。

3. 机器码生成与序列号校验的完整实现路径

状态机把“何时锁”“何时解锁”的逻辑想清楚了,接下来需要解决的是“怎么生成一个无法伪造的序列号”。这里拆成两部分:机器码是对客户身份的绑定,序列号是对开发者授权的证明。

3.1 机器码:从硬件信息到固定字符串的工程做法

机器码的作用是让一个序列号只对一台机器有效,避免客户用一套激活码在多台电脑上“无限复制”。我的方案是组合读取三项硬件信息,混合后用SHA-256摘要截取固定位。

具体读取方式上,LabVIEW里最省事的是调用系统命令再解析输出。第一种是CPU序号,推荐用PowerShell执行:

powershell -Command "Get-CimInstance Win32_Processor | Select-Object -ExpandProperty ProcessorId"

第二项是主板序列号:

powershell -Command "Get-CimInstance Win32_BaseBoard | Select-Object -ExpandProperty SerialNumber"

第三项是磁盘卷序列号,这个可以直接调用GetVolumeInformationAPI 拿到,在LabVIEW里通过Call Library Function Node链接kernel32.dll实现,返回的是一个32位无符号整数。

三项字符串里任意一项缺失或出现重复,就用其余两项目替换权重,最终得到一个唯一性还算可靠的原材料字符串。将原材料拼成CPU|BaseBoard|DiskVolume这样的格式,再取SHA-256摘要的前16个字节,转成32位十六进制字符串,中间用连字符分隔成类似A1B2-C3D4-E5F6-7890的格式,就是机器码。

这里要分享一个实战教训:依赖WMI查询CPU序号时,大量虚拟机或品牌定制机返回的是空字符串。比如某类云主机、瘦客户机上,ProcessorId空得像洗过一样,导致同型号机器码全部相同。所以我在读取时做了两道防御:第一优先顺序是CPU序号,如果为空则自动切换为“主板序列号+磁盘卷序列号”组合;如果主板序列号也为空,就再拼上网卡MAC地址和计算机名。宁可每个客户重装系统后需要重新申请序列号,也不能让多台机器共享同一个机器码。

3.2 序列号算法:用HMAC-SHA256替代简单异或

序列号本身不能只是机器码加密后的镜像,否则拿到序列号和机器码的人很容易推算出加密规则。如果你做的是长期项目,序列号算法一旦泄露,整个授权体系就废了。所以我把重心放在HMAC-SHA256上。

HMAC本质上是“加盐哈希”。你持有一个只有开发者知道的密钥(比如一段固定字符串YOUR_SUPER_SECRET_KEY_2025),然后把“机器码 + 授权到期日期 + 授权类型”打包成待签名消息,用密钥做HMAC计算。计算出的32字节摘要,哪怕只取签名的一部分,也能保证:没有密钥的人无法从机器码反推序列号,也无法篡改序列号里的到期日期。

序列号的具体格式我设计为:

授权类型前缀-到期日-签名截断值-机器码校验段

例如P1-20261231-8F3A2C1E-A1B2。其中:

  • P1表示永久授权;
  • 20261231是到期日(永久授权时为99991231);
  • 8F3A2C1E是HMAC摘要的前8个十六进制字符;
  • A1B2是机器码最后4位,方便客户肉眼对照。

校验时程序用本机机器码和序列号中提取的到期日后做同样的HMAC计算,再比对摘要前8位是否一致。为了防止用户手动改到期日然后重新输入蒙混过关,到期日必须参与签名运算,任何一位改动都会导致签名不匹配。

巧的是,LabVIEW的字符串处理加上 .NET 调用,写HMAC比想象中容易得多,关键在于调用节点选对成员函数。

3.3 LabVIEW调用.NET加密类的关键步骤与踩坑记录

LabVIEW原生没有HMAC-SHA256函数,但可以通过.NET节点间接调用,整套流程推荐在 LabVIEW 2023 及以上版本测试通过(低版本只要支持 .NET 4.0 也能跑)。

实现步骤大致如下:

  1. 在程序框图中放置一个Constructor Node,选择System.Security.Cryptography.HMACSHA256,创建实例。
  2. 创建一个 .NET 的System.Text.Encoding对象,把密钥字符串转换成 Byte 数组,赋值给 HMAC 实例的Key属性。
  3. 再把机器码和到期日拼接后的字符串同样编码成 Byte 数组。
  4. 调用HMACSHA256.ComputeHash(Byte[])方法,得到32字节哈希值。
  5. 用 LabVIEW 的Byte Array To Hex String转换函数,把哈希前8个字节转成十六进制字符串。

这里有两个坑,我当初各花了大半天才排查干净。

第一个坑是.NET ComputeHash 的重载选择。LabVIEW 调用重载方法时,连线板上有时会默认绑到其他重载上,导致传入 Byte 数组却报类型不匹配。解决办法是按名称在 .NET 节点属性区手动选择重载版本,或者把 Byte 数组包进UInt8[]类型后重新连线确认。

第二个坑是字符串编码。中文系统上如果直接使用默认编码,会把 ANSI 中文码位变成变化的内容,导致开发环境生成的序列号到了客户机器上校验失败。务必在发序列号小工具和 LabVIEW 校验模块中同时强制使用 UTF-8(System.Text.Encoding.UTF8),从源头上杜绝编码不一致。

每次激活流程结束后,务必释放 .NET 引用,否则程序重复运行激活窗口次数多了,内存占用会缓慢爬升。这点在长期不关闭的上位机软件里尤其明显,我遇到过连续按键20次激活按钮导致内存上涨上百兆的案例。

4. 防破解加固:时间回拨、文件删除与开发环境识别的实测对抗

序列号和机器码解决的是“伪造”问题,但实际交付后,你面对的更多是“篡改运行环境”型攻击。一个会电脑的普通人不会去逆向你的代码,他只会尝试三个操作:改系统时间、删文件、复制整个文件夹到另一台电脑。我们把这三条路逐一堵上,才算真正验收完这套授权模块。

4.1 改系统时间这招到底能不能绕过

先说结论:如果只做“记录首启日期,比较当前日期”,那肯定能绕过。演示场景如下:客户装上程序当天是2025年6月1日,试用期30天。6月25日他把系统日期改成5月20日,再启动软件,当前日期减首启日期变成了负数,试用期还剩很久。这就是典型的时间回拨漏洞。

我的应对逻辑在状态机部分已经说过,重点看一下“时间回拨检测”和“天数累计”之间的配合:

模块每次启动都执行三道检查:

  1. 当前日期是否早于最近一次成功运行的日期?如果是,锁定并提示时间异常。
  2. 如果日期合法,计算当前日期与首次运行日期的差值天数,超过试用天数则锁定。
  3. 如果客户试着在锁定期间把日期调到未来几天然后启动,程序会把“首次运行日期”视作最早基准,未来的启动日期会显著拉大天数差,锁定状态不可能被“跳过去”。

此外还有一个辅助手段:LabVIEW 的秒计数器Tick Count计算的是系统开机后的毫秒数,不受系统时间影响。如果有人在一段连续运行中手动改时间,程序内部可以用 Tick Count 估算运行时长,结合日期差异判断是否异常。不过实际项目里,这个辅助手段只作为参考,不做强判定,因为工控机常年不重启的情况下 Tick Count 会溢出,反而误报。

结合以上三重判定,客户想在试用期后续命,唯一可行的方案是永远不校时、永远不重启、每天顶着假日期运行。这种情况在真实生产环境里基本撑不过一周,因为多数上位机系统需要联网或与PLC同步,时间不对整个产线调度都会乱。真遇到那种“为了挪几天尾款把厂里时间全调乱”的对手,我个人会更倾向于建议直接用合同条款处理,而不是继续把技术防御做进赛博军备竞赛。

4.2 授权文件被删除后的自愈逻辑

既然授权状态存在三处,那客户删除其中一个文件会怎样?答案是:程序会利用多数一致原则自动修复,客户看到的只是界面上闪了一下,感知不到异常。真正让人头疼的是三个文件被同时删除、注册表也被清理干净,这时模块会像第一次安装一样重新进入试用。

为了避免这种“全套重置”操作被滥用,我的处理是:不把授权信息只藏在一个项目名下面,而是分拆成多个兼容用的键值,名字伪装成系统参数。这个思路听起来像“安全通过隐藏”,其实是为了增加操作门槛。普通客户很难想到HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SideBySide\Configuration这个无关键下某个值,会决定另一个软件什么时候锁死。

还有一个细节值得注意:文件存储时不要用明文文本,务必做一层异或或Base64偏移。虽然这不是密码学意义上真正的加密,但能防止客户用记事本打开lic.dat一眼看懂结构。我目前在项目中用的是“简单异或 + Base64”,密钥和机器码相关,能挡住绝大多数好奇的技术人员。

4.3 识别开发环境运行:防止有人用LabVIEW重新编译绕过

客户手里拿到的只是打包出来的exe,正常来说块图是不可见的。但如果有人打过LabVIEW主程序的主意,试图在你交付的文件夹里找到原始VI并重新编译,那启动路径会和正常打包后的程序有区别。为此,授权模块内置了一个简单的环境识别:

通过调用System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName拿到当前进程的完整路径,判断它是以LabVIEW.exe结尾(开发环境),还是以你的成品exe名称结尾。如果发现当前进程是 LabVIEW.exe,并且模块处于锁定状态,那么授权检查直接返回失败。这一招能在一定程度上防止客户把项目工程文件要过去后绕过授权。需要说明的是,这并不能防住真正专业的破解,但它的目的本来就不是跟破解者军备竞赛,而是杜绝“懂一点LabVIEW的客户技术人员顺手绕锁”的情况。

打包时我再额外做两件事:其一是在 Application Builder 里选择“加密VI”选项,避免Build后的exe被简单反编译出字符串;其二是把授权密钥拆成几个字符串片段,在运行时再拼接,使得在二进制文件里搜索YOUR_SUPER_SECRET_KEY_2025也搜不到完整内容。很多破解和搜索工具就是靠直接搜字符串定位关键逻辑的,这样处理之后至少能增加一层障碍。

4.4 接受合理的安全边界:防君子不防小人

做技术防护一定要有边界感。我在实践中得到的认知是:任何运行在客户本地的软件授权系统,都无法做到绝对不可破解,因为破解者拥有最终的执行环境。用冯老师的话说,你无法在一个由攻击者完全控制的机器上执行一个攻击者无法读取的算法。这不是LabVIEW的问题,是通用计算的本质问题。

因此,这套授权模块的设计目标是防偶然、防低门槛、防顺手,而不是对抗源代码级别的逆向工程。只要把时间回拨、文件删除、直接拷贝到另一台电脑这三条普通用户会走的路堵住,就已经能覆盖99%的实际商业纠纷场景。剩下的1%,靠前期合同、交付文档和验收单配合解决,技术手段和商务手段要各管一半。

5. 沉淀成可复用工具包:三个项目攒下来的工程化经验

单个项目的授权模块再完美,如果每次都要从零复制粘贴,维护成本也会吃掉节省下来的时间。我把前三个项目中写过的授权相关代码重新梳理了一遍,抽成了一个标准的授权工具包,后续新项目基本只需要调整公司名、产品名、授权天数这三个声明常量。

5.1 模块化架构:用LabVIEW Class组织授权逻辑

在工程结构上,我强烈建议用LabVIEW Class来封装,而不要散落成一堆VI放到vi.lib目录里到处引用。一个类的好处是状态数据可以内聚,外部主程序不需要关心授权仪表的内部实现。

类的私有数据我设计为:

  • CompanyName:注册表路径、存储文件名前缀;
  • ProductName:显示在激活窗口的软件名称;
  • TrialDays:试用天数,默认30;
  • HMACKey:开发者密钥;
  • InstallTime、LastRunTime、ExpireDate、LicenseType:授权状态数据;
  • CurrentState:前面说的四种状态枚举。

对外暴露的方法(Public Method)只需要五个:

  1. Initialize:读取所有存储位置,恢复或初始化授权状态;
  2. Check:执行一次状态判定,返回是否被锁定;
  3. GetMachineCode:返回本机机器码;
  4. Activate:输入序列号,执行校验并写入授权信息;
  5. ShowLockWindow:显示激活窗口,引导客户自助操作。

主程序调用时机极其简单:在顶层VI启动时,先调用Initialize和Check;如果结果为锁定,就调用ShowLockWindow循环等待,直到Activate成功或者用户退出程序。一些需要实时性的项目,还可以把Check放到主循环里,每隔10分钟或每个工步执行前再查一次,防止客户在程序持续运行期间修改时间或删除文件后“续命”。这个两级检查目前在我的所有交付项目里都是标配。

5.2 配套的序列号生成器:内部工具与交付流程

授权模块只负责校验序列号,而序列号由另一套独立工具生成。我写了一个很小的命令行生成器,用C#或者LabVIEW都行,输入三个参数:机器码、授权到期日、授权类型,输出一个激活码。

生成器的核心逻辑和我上面说的完全一致,唯一要注意的是开发者在不同机器上生成的密钥必须保持一致。如果你在办公室用一台电脑开发,到客户现场临时用笔记本生成序列号,两台机器的密钥字符串记得保持一致,否则生成的激活码无法通过校验。

序列号生成器的建议交互方式:

LicenseKeyGen.exe A1B2-C3D4-E5F6-7890 20261231 P1 P1-20261231-8F3A2C1E-A1B2

把这个工具放在自己有权限的目录里,不要放进项目源码库的公开分支。序列号属于商务交付物,出现泄露会直接影响收入,我一般只放在个人加密U盘或密码管理器里。

5.3 期满前后与客户的沟通节奏建议

技术方案落地之后,商务沟通方式也需要配套调整。我的习惯是:

  • 首版本交付时,在验收文档里明确写着“软件含试用期授权机制”,让客户知道试用期到哪天;
  • 试用期剩余7天时,在软件启动时弹窗提醒一次“授权将于7天后到期,如需续期请联系我”;
  • 剩余1天时再提醒一次;
  • 到期锁定后,激活窗口会展示机器码,并附带“获取序列号请联系项目负责人”等提示。

这样安排的好处是:客户不会觉得自己被暗算,因为到期前已有充分预告;而一旦拖到尾款阶段,激活窗口就会成为双方沟通的即时边界。从经验看,大多数合作方会在第2个提醒节点主动联系付款,因为谁也不想产线停摆。

5.4 后续扩展的三个方向

这个模块做到了第五版,我个人觉得还可以继续演进的方向有三个:第一,支持离线激活次数限制,比如一个机器码最多允许生成3个序列号,防止开发团队内部拿着生成器无限续期;第二,支持按功能模块授权,客户没付对应模块的钱,那部分控件就一直处于置灰状态;第三,把授权状态接入定时联网上报,客户装了软件之后,服务器端能看到最后一次运行时间,减少来回核对。

这些方向我还没有全部做完,卡点主要在于联网上报涉及客户现场的网络策略,并不是每家工厂都允许上位机访问外网。所以在工具包设计时,我把网络功能做成独立扩展,默认关闭,需要时再通过配置开关启用。这也算是一条务实经验:授权模块做得越复杂,本身出故障的概率就越高,过犹不及。优先保证基础的时间锁、序列号解锁、时间回拨防护这三个核心能力稳定可靠,再考虑加联网和功能开关,才是商业项目里最稳妥的推进路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询