.NET 程序保护实战系列02-字符串加密

02 · 字符串加密:让反编译器的搜索功能失效

目录

  1. 反编译器的"核武器":字符串搜索
  2. 整体流程:从 Ldstr 到 GetString
  3. 编码器模式:三种加密策略
  4. XOR 块加密与 xorshift32 密钥演化
  5. ID 编码:让索引看起来像随机数
  6. 不可见 Unicode 命名:注入代码的隐身术
  7. 整数常量加密:顺手保护数值
  8. 运行时 Constant 类:占位符驱动的动态模板
  9. 相比 ConfuserEx 的改进
  10. 结语

1. 反编译器的"核武器":字符串搜索

使用 dnSpy 打开一个未保护的 .NET 程序,按 Ctrl+F 搜索 "license"、"http"、"password"——几乎所有的关键信息都藏在字符串里。字符串加密是保护的第一道防线,目的就是让攻击者无法通过字符串搜索快速定位关键代码。


2. 整体流程:从 Ldstr 到 GetString

StringEncryptionStep 的核心逻辑

public class StringEncryptionStep : IProtectionStep
{
    public string Name => "字符串加密";
    public int Order => 10;
    public bool IsEnabled(ProtectOptions options) => options.EncryptStrings;

    public void Inject(ProtectionContext context)
    {
        // 1. 注入 Constant 运行时类型到目标模块
        // 2. 创建编码器(根据 StringMode: DelegateProxy/XorWithJunk/MultiLayer)
        // 3. 替换 Mutation.Crypt 占位符为真实解密 IL
        // 4. 内联 ID 解码到 GetString 方法
        // 5. 在 <Module>.cctor 开头插入 Constant.Initialize() 调用
        // 6. 用不可见 Unicode 重命名所有注入成员
        // 7. 标记注入成员为不可重命名
    }

    public void Execute(ProtectionContext context)
    {
        // 1. 扫描所有方法,收集 Ldstr 指令的字符串
        // 2. 编码字符串到加密缓冲区
        // 3. XOR 块加密缓冲区
        // 4. 替换每个 Ldstr 为 GetString(encodedId) 调用
        // 5. 可选:加密整数常量
    }
}

3. 编码器模式:三种加密策略

// DelegateProxy 模式 — 在解密和调用者之间插入委托层
typeof(Func<uint,string>).GetConstructor(new[] { typeof(object), typeof(IntPtr) })
// 静态分析只能看到 Invoke 委托,无法追踪到 DecodeString

// XorWithJunk 模式 — 追加随机垃圾字节
// 解密时返回正确子串,提取工具会被伪造内容误导

// MultiLayer 模式 — 双层加密
// 外层 XOR 块加密 + 内层字节置换,使用不同的密钥

4. XOR 块加密与 xorshift32 密钥演化

// 生成 16 个 uint 的解密密钥
static uint[] GenerateKey(uint seed)
{
    var key = new uint[0x10];
    uint n = seed;
    for (int i = 0; i < 0x10; i++)
    {
        n ^= n >> 12;
        n ^= n << 25;
        n ^= n >> 27;
        key[i] = n;
    }
    return key;
}

// 加密字符串数据
static uint[] Encrypt(uint[] data, uint seed)
{
    var key = GenerateKey(seed);      // 从种子生成密钥
    int n = 0;
    for (int i = 0; i < data.Length; i++)
    {
        // 每 16 个 uint 更换一次密钥
        uint k = key[n++ % key.Length];
        key[n % key.Length] = (data[i] ^ k);  // XOR 加密
        data[i] ^= k;
    }
    return data;
}

核心特点:

  • xorshift32 周期 2^32-1,性能极高
  • 每次保护使用不同的种子(随机选择某个方法入口点的 RVA)
  • 相同字符串在不同保护中产生完全不同的密文

5. ID 编码:让索引看起来像随机数

// 为每个 decoder 生成唯一的 (k1, k2) 编码密钥对
int k1 = random.Next(1, int.MaxValue);
int k2 = random.Next();

// 编码:encodedId = (realId * k1 + k2) & 0x7FFFFFFF
uint Encode(int id) => (uint)((id * k1 + k2) & 0x7FFFFFFF);

// 解码逻辑被内联到 GetString 方法中:
// realId = (encodedId - k2) * modInverse(k1) & 0x7FFFFFFF

没有 ID 编码的 GetString(0)GetString(1)GetString(2)... 会让攻击者轻易推断出字符串的顺序。编码后变成 GetString(0x8F3A2B1C)GetString(0x2E1D5C9A)...——看起来全是随机值。


6. 不可见 Unicode 命名:注入代码的隐身术

// 使用零宽 Unicode 字符作为注入成员的名称
// U+200B: Zero Width Space
// U+200C: Zero Width Non-Joiner
// U+200D: Zero Width Joiner
// U+2060: Word Joiner

// 注入后将类的原始名称替换为不可见字符组成的字符串
// 在 dnSpy 类视图中,这些成员看起来是"空"的
var invisibleName = GenerateInvisibleName(random);
typeDef.Name = invisibleName;
methodDef.Name = invisibleName;

同时标记注入成员为不可重命名,防止被后续混淆步骤改名后无法调用:

// 标记:此成员不可被改名
MarkAsNotRenameable(clonedMethod);

// ObfuscationStep 中检查
bool CanRename(IDnlibDef def) => !IsMarkedAsNotRenameable(def);

7. 整数常量加密:顺手保护数值

// ldc.i4 12345 → 拆分为:
// ldc.i4 (12345 ^ key)
// ldc.i4 key
// xor
foreach (var instr in body.Instructions)
{
    if (instr.OpCode == OpCodes.Ldc_I4)
    {
        int val = (int)instr.Operand;
        int key = random.Next();
        instr.Operand = val ^ key;         // 加密值

        // 在下一行插入 key 和 xor
        InsertAfter(instr,
            Instruction.Create(OpCodes.Ldc_I4, key),
            Instruction.Create(OpCodes.Xor));
    }
}

8. 运行时 Constant 类:占位符驱动的动态模板

运行时 Constant 类的核心 —— 利用 Mutation 占位符机制,在保护时动态替换关键值:

internal static class Constant
{
    // 占位符 —— 保护时会被 MutationHelper 替换为真实值和 IL
    // Mutation.KeyI0  → 替换为 buffer 长度
    // Mutation.KeyI1  → 替换为 seed
    // Mutation.Placeholder(...) → 替换为数组初始化 IL
    // Mutation.Crypt(...) → 替换为真实解密循环 IL

    static Func<uint, string> _fn;   // 委托代理层
    static byte[] _buf;              // 解密后的字符串缓冲区

    public static void Initialize()
    {
        // 以下占位符在保护时被替换为真实值
        uint len = (uint)Mutation.KeyI0;                        // → 真实数据长度
        uint seed = (uint)Mutation.KeyI1;                       // → 真实种子
        uint[] enc = Mutation.Placeholder(new uint[len]);       // → 真实密文数组

        uint[] key = GenerateKey(seed);
        for (int i = 0; i < len; i++)
            enc[i] = Mutation.Crypt(enc[i], key[i % 16]);      // → XOR 解密循环

        // 将解密后的数据复制到字节缓冲区
        _buf = new byte[enc.Length * 4];
        Buffer.BlockCopy(enc, 0, _buf, 0, _buf.Length);

        _fn = DecodeString;  // 委托绑定
    }

    // 公开入口 — 非泛型签名,模糊特征
    public static string GetString(uint id)
    {
        return _fn(id);  // 通过委托间接调用解码
    }

    // 实际解码 — 根据 id 从缓冲区提取字符串
    static string DecodeString(uint id)
    {
        // id 首先经过编码解码:realId = (id - k2) * invK1
        uint realId = Mutation.Placeholder(id);  // → 真实解码 IL
        // 从 _buf[offset] 读取 UTF-8 字符串
        int offset = BitConverter.ToInt32(_buf, (int)realId * 4);
        int length = BitConverter.ToInt32(_buf, offset);
        return Encoding.UTF8.GetString(_buf, offset + 4, length);
    }
}

Mutation 占位符类:

public static class Mutation
{
    // 16 个 key 占位字段 — 保护时替换为 ldc.i4 <真实值>
    public static int KeyI0, KeyI1, KeyI2, KeyI3, KeyI4, KeyI5,
                      KeyI6, KeyI7, KeyI8, KeyI9, KeyI10, KeyI11,
                      KeyI12, KeyI13, KeyI14, KeyI15;

    // 占位方法 — 保护时替换为数组初始化指令序列
    public static T Placeholder<T>(T val) => val;

    // 占位方法 — 保护时替换为 Crypt IL 实现
    public static uint Crypt(uint val, uint key) => 0;
}

9. 相比 ConfuserEx 的改进

特性 ConfuserEx 我们的实现
压缩 LZMA 压缩 直接 XOR 加密(更轻量,更快)
GetString 方法 泛型 Get<T>(int) — 签名特征明显 非泛型 GetString(uint) — 更隐蔽
注入成员命名 保留原始名称 "Constant" 不可见 Unicode 名称
ID 编码 无 — 直接传递索引 (k1, k2) 编码密钥对
委托代理 DelegateProxy 模式
整数常量加密 支持 Ldc_I4/I8 XOR 拆分
安全检查 验证调用方程序集
参数类型保护 支持 I4/I8/U4/U8/R4/R8

10. 结语

字符串加密是最基础但最有效的第一道防线。它让攻击者无法通过 Ctrl+F 定位关键代码,配合不可见 Unicode 命名,进一步隐藏保护代码的存在。Mutation 占位符机制是整个保护引擎的核心设计模式——运行时模板带占位符,保护时动态替换——后续的方法体加密、虚拟化、防篡改都依赖同样的模式。

下一篇将讲解符号混淆——如何让类名、方法名变成天书。


本文由 TWSoft.AssemblyProtector 驱动。完整源码和工具请访问项目主页。

posted @ 2026-07-06 12:08  翼帆  阅读(16)  评论(0)    收藏  举报