.NET 程序保护实战系列 08 · 防篡改校验:任何修改都会触发自毁

📅 2026/7/11 22:14:39
.NET 程序保护实战系列 08 · 防篡改校验:任何修改都会触发自毁
问题保护后的程序可以被再次修改即使经过了所有保护层攻击者仍然可以修改程序——替换一个方法体为 ret、修改 IL 实现功能绕过。防篡改校验是最后的防线检测到任何修改 → 立即自毁。FNV-1a 哈希每个方法的指纹选择 FNV-1a 32-bit 的原因极快2 次 XOR 1 次乘法碰撞风险低。// 保护时计算哈希AntiTamperStepstatic uint ComputeHash(byte[] ilBytes, uint token){uint hash 2166136261u; // FNV offset basishash ^ token; // 混入方法 token阻止批量替换for (int i 0; i ilBytes.Length; i){hash ^ ilBytes[i];hash * 16777619u; // FNV prime}return hash;}以方法 token 为种子的关键作用每个方法的哈希种子不同攻击者无法将所有方法替换为同一个 stub 后统一伪造哈希3. 两阶段写入策略保证哈希一致性获取方法体 IL 字节需要将模块写完——但写完就不能再修改。我们的两阶段写入策略解决这个问题public void Execute(ProtectionContext context){// 第一阶段提取 IL // 1. 将当前模块状态写入 MemoryStream不写磁盘using var ms new MemoryStream();context.Module.Write(ms, new ModuleWriterOptions(context.Module){Logger DummyLogger.NoThrowInstance // 允许部分未完成的状态});ms.Position 0;// 2. 用 PEReader 从写入后的模块中读取每个方法的 IL using var peReader new PEReader(ms); var mdReader peReader.GetMetadataReader(); foreach (var methodHandle in mdReader.MethodDefinitions) { var method mdReader.GetMethodDefinition(methodHandle); var ilReader peReader.GetMethodBody(method.RelativeVirtualAddress); byte[] il ilReader.GetILContent(); // 3. 计算 FNV-1a 哈希以 token 为种子 uint hash ComputeHash(il, (uint)methodHandle.ToToken()); hashes.Add(hash); } // 第二阶段存储哈希表 // 4. 序列化 token 和 hash 对应表 // 5. XOR 加密 // 6. 以魔术头 0xDEADBEEF 添加为 ManifestResource}4. XOR 加密 魔术头隐藏哈希表// LCG XOR 加密与运行时 Decrypt 互逆static void Encrypt(byte[] data, int offset, uint key){uint state key;for (int i offset; i data.Length; i){state state * 214013 2531011; // LCG: Microsoft Visual C rand()data[i] ^ (byte)(state 16);}}// 资源数据格式// [4 bytes] 0xDEADBEEF (魔术头)// [4 bytes] seed (LCG seed)// [4 bytes] count (方法数量)// [foreach method]:// [4 bytes] methodToken// [4 bytes] expectedHash运行时不依赖资源名称而是扫描所有 ManifestResource 找到魔术头foreach (var name in asm.GetManifestResourceNames()){using var stream asm.GetManifestResourceStream(name);byte[] header new byte[4];stream.Read(header, 0, 4);if (header[0] 0xDE header[1] 0xAD header[2] 0xBE header[3] 0xEF){// 找到了文件名可以是任何随机值return ReadData(stream);}}5. 运行时 AntiTamper.Initializeinternal static class AntiTamper{public static void Initialize(){var asm Assembly.GetExecutingAssembly();var module asm.ManifestModule;// 1. 扫描所有资源查找魔术头 0xDEADBEEF byte[] data null; foreach (var resName in asm.GetManifestResourceNames()) { byte[] buf; using (var stream asm.GetManifestResourceStream(resName)) { buf new byte[stream.Length]; stream.Read(buf, 0, buf.Length); } if (buf[0] 0xDE buf[1] 0xAD buf[2] 0xBE buf[3] 0xEF) { data buf; break; } } if (data null) return; // 2. 解密 uint key BitConverter.ToUInt32(data, 4); for (int i 8; i data.Length; i) { key key * 214013 2531011; data[i] ^ (byte)(key 16); } // 3. 逐方法重新计算哈希并比对 int count BitConverter.ToInt32(data, 8); int pos 12; for (int i 0; i count; i) { int token BitConverter.ToInt32(data, pos); pos 4; uint stored BitConverter.ToUInt32(data, pos); pos 4; var method module.ResolveMethod(token); var body method.GetMethodBody(); byte[] il body.GetILAsByteArray(); uint actual ComputeHash(il, (uint)token); if (actual ! stored) { // 任何不匹配 → 立即终止 Environment.FailFast(null); return; } } } // FNV-1a与保护时完全一致 static uint ComputeHash(byte[] il, uint token) { uint hash 2166136261u ^ token; for (int i 0; i il.Length; i) { hash ^ il[i]; hash * 16777619u; } return hash; }}6. Environment.FailFast不可捕获的终止// 为什么不用 throw 或 Environment.Exit// throw new Exception() → 可被 try-catch 捕获// Environment.Exit(n) → 可被 AppDomain.ProcessExit 钩住// Environment.FailFast → 不可捕获、写事件日志、即时终止Environment.FailFast(“Assembly has been tampered with.”);攻击者无法通过 try-catch、AppDomain 钩子或 finally 块绕过。