.NET 程序保护实战系列 08 · 防篡改校验:任何修改都会触发自毁
08 · 防篡改校验:任何修改都会触发自毁
目录
- 问题:保护后的程序可以被再次修改
- FNV-1a 哈希:每个方法的指纹
- 两阶段写入策略:保证哈希一致性
- XOR 加密 + 魔术头:隐藏哈希表
- 运行时 AntTamper.Initialize
- Environment.FailFast:不可捕获的终止
- 结语
1. 问题:保护后的程序可以被再次修改
即使经过了所有保护层,攻击者仍然可以修改程序——替换一个方法体为 ret、修改 IL 实现功能绕过。防篡改校验是最后的防线:检测到任何修改 → 立即自毁。
2. FNV-1a 哈希:每个方法的指纹
选择 FNV-1a 32-bit 的原因:极快(2 次 XOR + 1 次乘法),碰撞风险低。
// 保护时计算哈希(AntiTamperStep)
static uint ComputeHash(byte[] ilBytes, uint token)
{
uint hash = 2166136261u; // FNV offset basis
hash ^= 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.Initialize
internal 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 块绕过。
7. 结语
防篡改校验是保护链条的最后一环——即使前面的保护层被突破,篡改后的程序也无法运行。两阶段写入策略保证了哈希的精确性,魔术头扫描避免资源名称泄露信息,Environment.FailFast 确保不可绕过。
下一篇将讲解反调试与反转储——在运行时层面防止分析和内存提取。
本文由 TWSoft.AssemblyProtector 驱动。完整源码和工具请访问项目主页。

浙公网安备 33010602011771号