哈希校验只防篡改,不防盗版复制
直接说结论:
MD5、
SHA256这类哈希值对文件内容敏感,但只要文件字节完全一致,哈希就一样——盗版者原样复制你的文件,哈希毫无察觉。它只能告诉你“这文件没被改过”,不能回答“这文件是不是从我这儿流出去的”。
常见错误现象:用
File.ReadAllBytes算出哈希后硬编码进程序,以为能识别“非官方分发版本”;结果用户把正版文件拷走,照样运行成功。 适用场景:验证安装包完整性、检查配置文件是否被意外修改 不适用场景:追踪文件来源、区分授权用户、对抗有意分发行为 性能影响极小,但误用会导致安全错觉
C# 中嵌入不可见水印需修改文件结构
真正能指向来源的水印,必须在文件内部藏点“只有你知道”的信息。纯文本或 XML 文件可以加注释(如
<!-- licensed_to: user_123 -->),但二进制文件(如 EXE、DLL)得动字节——比如在 PE 文件的未使用节区、资源段末尾或证书表空隙里写入自定义数据。
实操建议:
不要往代码段或入口点附近写,容易触发杀毒软件误报 用System.IO.FileStream定位到 PE 文件的
.rsrc节末尾,追加 64 字节 base64 编码的客户 ID + 时间戳 读取时用
ImageDosHeader和
ImageNtHeaders解析节偏移,避免硬编码位置 注意:.NET Core / .NET 5+ 发布的单文件应用(
publish-self-contained=true)会打包成压缩归档,水印需在打包前注入
运行时检测水印比静态扫描更可靠
把水印藏在文件里只是第一步,关键是怎么在程序启动时悄悄把它捞出来验证。静态扫描(比如另起一个工具去读 EXE)容易被绕过;而让程序自己在
Main函数最开头读自身文件、提取水印并联网校验,才是实际可行的做法。
常见坑:
Assembly.GetExecutingAssembly().Location在 ClickOnce 或某些容器中返回的是临时路径,不是原始 EXE 位置 用
Process.GetCurrentProcess().MainModule.FileName更稳妥,但需要
System.Diagnostics权限 水印解码失败时别直接退出,记日志并降级为匿名模式——否则用户第一反应是删掉你的日志模块 别用明文存客户 ID;至少用项目专属密钥做 AES-ECB 加密(ECB 不安全但够用,因水印本身不承载高敏数据)
水印和授权绑定必须服务端参与
所有客户端能读到的信息,理论上都能被提取和伪造。所以水印字段(比如
license_id)必须和服务端可查的状态联动:服务端要记录该 ID 是否激活、是否被吊销、绑定设备数是否超限。
关键细节:
客户端只传水印内容,不传任何签名或密钥;签名由服务端生成并下发(例如 JWT 里带有效期和硬件指纹) 首次运行时若水印 ID 为空或格式非法,应引导用户输入授权码,而不是拒绝启动 本地缓存水印校验结果必须设短时效(如 15 分钟),防止断网后永久免检 别把水印校验逻辑全写在客户端;哪怕只是简单 HTTP GET 请求,也要让服务端决定“这个水印现在算不算合法”水印不是开关,是线索。真正拦住盗版的,永远是服务端对线索的实时裁决,以及客户端对裁决结果的诚实执行。漏掉任意一环,就只剩心理安慰。
