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

08 · 防篡改校验:任何修改都会触发自毁

目录

  1. 问题:保护后的程序可以被再次修改
  2. FNV-1a 哈希:每个方法的指纹
  3. 两阶段写入策略:保证哈希一致性
  4. XOR 加密 + 魔术头:隐藏哈希表
  5. 运行时 AntTamper.Initialize
  6. Environment.FailFast:不可捕获的终止
  7. 结语

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 驱动。完整源码和工具请访问项目主页。

posted @ 2026-07-10 16:49  翼帆  阅读(76)  评论(0)    收藏  举报