简介:一套面向软件开发者的“一机一码”注册机生成与EXE、视频文件加密解决方案,专注解决软件授权绑定机器码、防止文件被破解复制等问题,适合需要给程序增加license保护或加密视频资源的开发者参考。压缩包共111个文件,容量仅4.54MB,类型覆盖C++、Delphi、VB、汇编等多种语言源码(cpp/pas/bas/asm),配合工程文件(vcproj/dpr/vbp/sln)、已编译的exe/dll/lib、资源定义文件、chm帮助文档及批处理脚本,结构清晰便于检索。包含FASM汇编示例、Potato等多语言示例和自动编译脚本,可对照源码理解注册机生成、机器码采集绑定以及视频文件加密的完整流程;既可直接运行体验效果,也能修改复用其核心代码,快速搭建自己的授权体系。已有3101人学习下载,适合对软件加密保护、防逆向和授权机制感兴趣的中高级开发者。 上个月我在闲鱼刷到自己的付费课程被二道贩子打包转卖,附带一个“破解版播放器”,评论区还有人问“老板能不能离线看”。那个播放器是我两年前写的第一版工具,当时只加了个密码校验,被扒下来也就是几分钟的事。那晚我没急着去吵架,而是在笔记本上列了一份需求清单:一机一码绑定硬件、视频文件加密、授权码自动生成,这次要做就做一整套,还都得自己掌控。
这篇文章会按我落地这套方案的顺序来讲,包括机器码怎么取最靠谱、注册机该用什么签名算法才不容易被逆向、Python 脚本到底怎么打包成 EXE 才少踩坑,以及视频加密和授权验证怎么在一个播放器里联动。适合被盗版问题困扰的独立开发者、准备给客户做授权交付的桌面软件作者,也适合想入门软件授权体系设计的同学。
1. 一机一码的授权链路:先搞清楚软件是被谁“抄走”的
先说一个重要判断:一机一码防的主要不是逆向高手,而是“复制粘贴”。大部分普通用户不会反编译你的 EXE,他们只是觉得“这软件挺好,传给同事一起用”“这课程不错,发给闺蜜看看”。一对多的复制,靠一个密码框根本拦不住。一机一码要做的事情,是把每一个合法的授权绑定到一台具体的设备上,让复制行为在逻辑上失效。
1.1 授权链路里的三个角色
一套典型的一机一码体系里有三个角色:
- 客户端程序:跑在用户电脑上,负责生成机器码、接收注册码、验证授权状态。
- 注册机:跑在软件作者自己电脑上的专用工具,输入机器码后输出合法的注册码。
- 用户:把客户端生成的机器码发给你,再从你这里拿到注册码输入回软件。
关键点在于,注册机永远不能出现在用户手里,也不能混进客户端安装包。它是作者私有的“发牌器”,一发一个准,并且发的每一张牌都只在对应的那台机器上有效。
1.2 完整的激活时序
在动手写代码之前,我先把流程画得非常具体:
- 用户首次运行客户端,程序采集硬件指纹,经过哈希处理后生成一串机器码,显示在“关于/激活”窗口里。
- 用户把这串机器码通过微信、邮件或表单发给你。
- 你在自己电脑的注册机界面粘贴机器码,再选一个授权等级和过期时间,点击生成,得到注册码。
- 注册码是一段经过签名的长字符串,用户复制到客户端,客户端用内置公钥验证签名。
- 验证通过后,客户端在本机保存一个授权文件,记录机器码、有效期、等级。后续每次启动都校验一次。
这个链路里有三个技术点必须想清楚:机器码会不会变、注册码能不能被伪造、授权文件能不能被复制到别的电脑。后面三节我就是逐个解决这三个问题的。
2. 机器码采集:每台电脑的“身份证”怎么取才不翻车
机器码是整个体系的地基。如果两台不同电脑生成了同样的机器码,那你的注册码就可以全国通用;如果同一台电脑每次生成的结果都不一样,用户会被你气死。稳,是第一原则。
2.1 哪些硬件信息适合做指纹
我试过很多组合,最后稳定使用的候选信息有这么几类:
- CPU 序列号(ProcessorId)
- 主板序列号(BaseBoard SerialNumber)
- 第一块物理磁盘的序列号(DiskDrive SerialNumber)
- MAC 地址
另外一个值得谨慎使用的是 Windows 安装时生成的 MachineGuid 注册表项,它位于HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography。这个值在系统未重装的情况下是稳定的,而且每台机器几乎必然不同;坏处是一旦重装系统就会变。我的处理方式是把 MachineGuid 作为辅助字段加入哈希,而不是唯一依赖。
注意:网卡 MAC 在休眠、插拔 USB 网卡、启用虚拟网卡时都可能变化,所以不能只用它一个。CPU 和主板序列号在生产环境里确实有极少数返回空值的情况,我补了空值默认策略。
2.2 机器码生成的 Python 实现
我最终用的是 wmic 加保底方案。Python 环境是 3.10 + pywin32,核心代码如下:
import hashlib import subprocess import uuid def _query_wmic(args): try: result = subprocess.check_output( "wmic " + args, shell=True, stderr=subprocess.DEVNULL ).decode("utf-8", errors="ignore").strip() lines = [line.strip() for line in result.splitlines() if line.strip()] return lines[1].strip() if len(lines) > 1 else "UNKNOWN" except Exception: return "UNKNOWN" def get_cpu_id(): return _query_wmic("cpu get processorid") def get_board_id(): return _query_wmic("baseboard get serialnumber") def get_disk_id(): return _query_wmic("diskdrive get serialnumber") def get_machine_guid(): try: import winreg key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") value, _ = winreg.QueryValueEx(key, "MachineGuid") return str(value).strip() except Exception: return "UNKNOWN" def generate_machine_code(): info = [ get_cpu_id(), get_board_id(), get_disk_id(), get_machine_guid(), str(uuid.getnode()) ] raw = "|".join(info) return hashlib.sha256(raw.encode("utf-8")).hexdigest().upper()为什么不直接展示原始硬件信息,而是做 SHA256?原因很简单:客户端机器码如果直接等于“CPU序列号-主板序列号”,一旦软件被反编译,这些硬件信息就暴露了,会加剧隐私风险。哈希之后就只是 64 位十六进制串,用户也更容易复制,不容易漏字符。
2.3 机器码稳定性的实测结果
我在三台机器做了跨周测试:两台 Windows 10,一台 Windows 11。结果如下:
| 测试项 | 结果 |
|---|---|
| 重启后重新生成 | 全部一致 |
| 拔掉外接 USB 网卡 | 全部一致 |
| 关闭/开启虚拟机网卡 | 全部一致 |
| 插入新的 U 盘 | 全部一致 |
| 重装系统后 | MachineGuid 变化,若该项目进入哈希则结果变化 |
这个结果让我决定:MachineGuid 不起主决定作用,但参与哈希有助于区分“两台配置完全一致的品牌机”。它们虽然 CPU/主板序列号不同,但这个差异本应已经足够;遇到极端情况,MachineGuid 能兜底。
3. 注册机核心设计:RSA 签名让注册码不可伪造
如果只把机器码存到本地文件里,那人家直接复制整个授权文件到另一台电脑,是不是就绕过了一机一码?所以授权内容必须和机器码绑定,而且不能让用户手工拼接篡改。
3.1 为什么我不选择 HMAC 对称方案
常见的错误做法是把机器码和一个固定密钥做 HMAC,生成一段“签名”作为注册码,客户端再用同一个密钥验证。这样做验证端和生成端拥有同一个密钥,只要有人反编译客户端,提取出密钥,他就能写一个和你的注册机等效的生成脚本。这是对称方案在客户端分发场景里的致命伤。
所以我选择了 RSA 非对称签名:注册机持有私钥,只在你自己的电脑上;客户端只放公钥,即使公钥被提取出来,也做不了任何签名。这是整个体系防伪造的根基。
3.2 注册码的数据结构
一条注册码在我这里被组织成这样的结构:
base64( 机器码 | 过期日期 | 授权等级 | RSA签名 )签名前的内容是明文的,签名是对内容哈希之后做的。为什么要带过期日期和授权等级?因为我可以把“卖一年授权”和“永久授权”区分开,并且把“标准版”和“专业版”都塞进这条码里。用户改注册码里的日期?验签直接失败。
3.3 注册机端的签名代码
注册机不发布,只在你自己的 Windows 电脑上跑,可以用纯 Python 写个带界面的小工具。核心逻辑:
import base64 from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 # 生成密钥对,只在初始化时执行一次 # key = RSA.generate(2048) # private_pem = key.export_key().decode() # public_pem = key.publickey().export_key().decode() private_key = RSA.import_key(open("private.pem").read()) def create_license(machine_code, expire_date, level): payload = f"{machine_code}|{expire_date}|{level}" digest = SHA256.new(payload.encode("utf-8")) signature = pkcs1_15.new(private_key).sign(digest) token = base64.b64encode(payload.encode("utf-8") + b"|" + signature) return token.decode("utf-8")这里有个容易踩的坑:签名是二进制数据,直接拼到字符串里会乱码。我先把 payload 编码成 UTF-8 bytes,再拼上分隔符|和二进制签名,最后整体做 base64。这样注册码就是一段可复制、可传输的 ASCII 文本。
3.4 客户端验签代码
客户端里放的是公钥,验证流程是对注册码先解码,再从右侧最后一个|处拆出签名和载荷:
import base64 from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 public_key = RSA.import_key(open("public.pem").read()) def verify_license(token): try: raw = base64.b64decode(token.encode("utf-8")) payload, signature = raw.rsplit(b"|", 1) digest = SHA256.new(payload) pkcs1_15.new(public_key).verify(digest, signature) machine_code, expire_date, level = payload.decode("utf-8").split("|") return True, machine_code, expire_date, level except Exception: return False, "", "", ""拿到验签后的机器码,客户端还会和本机当前generate_machine_code()的结果做一次比对,二者一致才放行。这一步很关键,用户如果把注册码发给别人,对方就算粘贴到自己软件里,也会因为机器码不匹配而激活失败。
4. 客户端 EXE 打包与防反编译的取舍
技术上打通后,下一步是让用户拿到一个能双击运行的 EXE。我最初用 PyInstaller 打包,过程比想象中顺利,但也踩了几个不大不小的坑。
4.1 PyInstaller 打包命令
我的客户端依赖 pycryptodome、wmi、subprocess 等模块,打包命令如下:
pyinstaller --noconfirm --onefile --windowed --name VideoGuard client.py如果你像我一样在代码里动态读取了公钥文件public.pem,记得把这个文件一起打进去。最简单的方式是修改生成的.spec文件里的datas,例如:
a.datas += [("public.pem", "public.pem", "DATA")]然后执行pyinstaller VideoGuard.spec,这样公钥会随 EXE 一起被释放到临时目录,软件运行时就能读取到。
4.2 反编译风险:明白极限在哪里
Python 写的 EXE 确实能被反编译出字节码,再还原出接近源码的代码。这意味着公钥、授权验证逻辑都可能被逆向者看到。但我上面说了,客户端只有公钥,它本身做不了签名。反编译对“注册码伪造”的危害远没有想象中那么大。
为了增加一点逆向成本,我在发布前用了 Nuitka 做二次编译,把关键逻辑转成 C 再编回二进制。Nuitka 的编译时间比较久,第一次全量编译花了二十多分钟,但生成物确实比单纯 PyInstaller 的 pyc 要难分析得多。编译命令:
nuitka --onefile --enable-plugin=tk-inter --include-data-file=public.pem=public.pem client.py如果你是做技术验证的 Demo,PyInstaller 就够了;如果产品要正式售卖,建议至少走一遍 Nuitka,性价比比外部加壳高得多。
4.3 UPX 加壳与杀毒误报的拉锯
为了减小体积,我一度给 PyInstaller 开了 UPX 压缩,结果 Windows Defender 直接报毒,而且不止在一台机器上。后来查到原因:UPX 解压特征和部分加壳恶意软件高度重叠,误报率极高。最终我放弃了 UPX,靠 Nuitka 的产物本身已经足够用。这里也提醒一句:打包工具越“冷门”,误报率往往越低,但代价是你自己维护起来也要多花时间。
5. 视频文件加密:离线播放器怎么守住内容
软件授权解决的是“谁能用”,视频加密解决的是“内容怎么才能不被直接拿走”。视频文件如果只是个普通 mp4,用户激活后顺手把文件拷贝出去,授权体系就白做了。我的思路是:把视频数据加密,播放时必须由客户端实时解密,并且解密动作依赖授权验证结果。
5.1 分块加密方案:AES-CTR 模式
视频文件通常很大,一次性整体解密既不现实,也不利于拖动进度条。我用 AES-256-CTR 模式对视频做分块加密。CTR 模式是把一个计数器和密钥一起输入块密码生成密钥流,再和明文异或;解密时使用完全相同的计数器和密钥就能还原明文。它的最大优点是支持随机访问:我想解密视频的第 5 个分块,只需要把计数器调整到第 5 块的位置,不需要从头解密到第 5 块。
加密工具代码:
from Crypto.Cipher import AES from Crypto.Util import Counter import os def encrypt_video(src_path, dst_path, key): iv = os.urandom(16) initial_counter = int.from_bytes(iv, "big") ctr = Counter.new(128, initial_value=initial_counter) cipher = AES.new(key, AES.MODE_CTR, counter=ctr) with open(src_path, "rb") as fin, open(dst_path, "wb") as fout: fout.write(iv) while True: chunk = fin.read(1024 * 1024) if not chunk: break fout.write(cipher.encrypt(chunk))解密端读取前 16 字节得到 IV,然后构建同样的 Counter,边读边解密。如果要做进度条 seek,可以先计算目标字节位置对应的分块计数偏移,再重新构造 Counter。
5.2 密钥不落地
加密视频的密钥不能是硬编码在播放器里的常量,否则反编译后谁都可以解密。我的做法是:密钥单独保存为一个密文密钥文件,里面是用公钥加密后的“内容加密密钥”。播放器在启动时,先验证注册码、比对机器码,全部通过后,用客户端内置私钥去解出内容密钥,再流式解密视频。这样即使有人拿走了视频文件和密钥文件,没有授权和对应私钥,也无法解密出明文。
说明:客户端内置私钥还是可以被最强的逆向手段提取,这是所有 DRM 类方案都绕不开的物理极限。但对 99% 的复制传播场景,这套防护已经足够让普通用户知难而退。
5.3 如果不想自己写播放器
做一个完整的视频播放器工作量不小。如果你只是想把加密视频放到现有播放器里放,我建议直接用 FFmpeg 的 HLS AES-128 方案:先对视频切片,再用密钥文件对每一个.ts切片加密,生成m3u8播放列表。播放器端只有拿到正确密钥才能播放。你的客户端要做的事情,就是验证授权后把 key 交给播放器,明文的 key 始终只在内存中。
FFmpeg 生成加密 HLS 的关键参数示意:
ffmpeg -i input.mp4 -hls_time 10 -hls_playlist_type vod -hls_key_info_file key_info.txt output.m3u8其中key_info.txt里配置了密钥文件路径、加密后的 key 的 URL 和 IV。HLS 方案兼顾了流媒体兼容性和切片加密,比整文件加密更符合当前播放生态。
6. 整套系统上线后的实测与三个印象最深的坑
方案在代码里走通是一回事,真正部署到各种用户电脑上又是另一回事。我自己在测试群里发过 20 个试用授权,收到的反馈让我改了好几版。
6.1 机器码采集的兼容性问题
最集中的问题是老旧的 Windows 10 精简版系统上,wmic 命令可能被精简掉。第一次测试时我就翻车了:一个用户反馈生成机器码时程序闪退。排查发现是子进程调用返回异常,而我的异常处理没有涵盖stdout为空的情况。后来我把_query_wmic里所有可能的返回值都加了兜底,并且启动时先做一次自检,如果 CPU、主板、磁盘全部为 UNKNOWN,就直接提示“当前系统不支持硬件指纹采集”并给出人工通道。绝不能放任程序崩溃。
6.2 注册码长度与人工发送的体验
RSA 2048 位签名,再加上机器码、日期、等级信息,最终注册码长度在 400 个字符左右。用户从微信复制这种字符串时很容易漏掉尾部字符。我后来做了两个优化:注册码全部大写;客户端激活框里加入自动去除空格和换行、逐段校验的提示。另外在注册机里我加了二维码输出,用户扫码直接复制到剪贴板,这个改动极大降低了人工输入错误率。
6.3 时间回拨和重装系统的处理
注册码里有过期时间,客户端每次启动都要校验。有用户为了续期,把系统时间拨回去,结果授权确实重新有效了。这个问题的通用解法是引入一个可信时间源服务,但让单机工具每次联网校验又会招致离线用户的反对。我采用的折中方案是:客户端把上次成功校验的系统时间和当前系统时间的差值记录下来,如果差值超过 24 小时,就强制要求重新激活。重装系统的情况更复杂,我选择允许用户向作者临时申请一个“重装码”,会根据硬件哈希匹配情况决定是否通过,避免正版用户被误伤。
6.4 授权文件被整体复制的问题
最后再讲一个非常容易被忽略的漏洞:用户把激活过的整个软件目录压缩包发给朋友,里面包括授权文件。如果授权文件里只存“已激活”标志,对方解压后就能直接用了。所以我在授权文件里不仅存验签后的机器码,还在每次启动时重新计算当前机器的机器码做比对。只有授权文件里的机器码和当前机器一致,软件才继续运行。这样即使授权文件整包被复制走,也会因为硬件指纹不匹配而失效。
7. 一些更省事的后续扩展思路
整套体系跑通之后,我又做了几个小扩展,纯粹是顺手,但效果不错。
注册机我后来加了一个“批量发码”功能:从 Excel 粘贴一串机器码和对应的到期日,一键生成全部注册码列表,再自动复制到剪贴板。要发的授权多了以后,这个功能能省下不少时间。
客户端激活界面也做了升级:先显示机器码和复制按钮,用户发给你的时候不用手敲;激活成功后显示到期日;到期前 7 天每次启动弹一次提醒,配合续费流程体验比较顺。另外我还给注册码加了一个更直观的“授权人姓名”字段,这样授权可追溯,也能防止别人拿你的注册码到处秀。
最后想说的是,这套方案不是银弹。最核心的价值在于把“复制就能用”变成“复制了也用不了”,把盗版成本抬高到一个普通人懒得跨过去的水平。我在实际运营中看到的主要变化是:转卖资源的人还在,但转卖以后买家装不上、打不开,反而会回去骂卖家,这本身就让盗版链条的转化率跌了一大截。对独立开发者来说,这已经是一个相当值得投入的成本收益平衡点了。
本文还有配套的精品资源,点击获取