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)
{
}
public void Execute(ProtectionContext context)
{
}
}
3. 编码器模式:三种加密策略
typeof(Func<uint,string>).GetConstructor(new[] { typeof(object), typeof(IntPtr) })
4. XOR 块加密与 xorshift32 密钥演化
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++)
{
uint k = key[n++ % key.Length];
key[n % key.Length] = (data[i] ^ k);
data[i] ^= k;
}
return data;
}
核心特点:
- xorshift32 周期 2^32-1,性能极高
- 每次保护使用不同的种子(随机选择某个方法入口点的 RVA)
- 相同字符串在不同保护中产生完全不同的密文
5. ID 编码:让索引看起来像随机数
int k1 = random.Next(1, int.MaxValue);
int k2 = random.Next();
uint Encode(int id) => (uint)((id * k1 + k2) & 0x7FFFFFFF);
没有 ID 编码的 GetString(0)、GetString(1)、GetString(2)... 会让攻击者轻易推断出字符串的顺序。编码后变成 GetString(0x8F3A2B1C)、GetString(0x2E1D5C9A)...——看起来全是随机值。
6. 不可见 Unicode 命名:注入代码的隐身术
var invisibleName = GenerateInvisibleName(random);
typeDef.Name = invisibleName;
methodDef.Name = invisibleName;
同时标记注入成员为不可重命名,防止被后续混淆步骤改名后无法调用:
MarkAsNotRenameable(clonedMethod);
bool CanRename(IDnlibDef def) => !IsMarkedAsNotRenameable(def);
7. 整数常量加密:顺手保护数值
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;
InsertAfter(instr,
Instruction.Create(OpCodes.Ldc_I4, key),
Instruction.Create(OpCodes.Xor));
}
}
8. 运行时 Constant 类:占位符驱动的动态模板
运行时 Constant 类的核心 —— 利用 Mutation 占位符机制,在保护时动态替换关键值:
internal static class Constant
{
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]);
_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);
}
static string DecodeString(uint id)
{
uint realId = Mutation.Placeholder(id);
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
{
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;
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 驱动。完整源码和工具请访问项目主页。
转自https://www.cnblogs.com/gsyifan/p/21161615
该文章在 2026/7/10 8:20:09 编辑过