UWP 程序有 .NET Native 可以将程序集编译为本机代码,逆向的难度会大很多;而基于 .NET Framework 和 .NET Core 的程序却没有 .NET Native 的支持。虽然有 Ngen.exe 可以编译为本机代码,但那只是在用户打算机上编译完后放入了缓存中,而不是在开拓者端编译。
于是有很多款稠浊工具来帮助稠浊基于 .NET 的程序集,使其轻微难以逆向。本文先容 Smart Assembly 各项稠浊参数的浸染以及其实际对程序集的影响。
本文不会讲 SmartAssembly 的用法,由于你只需打开它就能明白其基本的利用。
感兴趣可以先下载:.NET Obfuscator, Error Reporting, DLL Merging - SmartAssembly。
准备我们先须要准备程序集来进行稠浊试验。这里,我利用 Whitman 来试验。它在 GitHub 上开源,并且有两个程序集可以试验它们之间的相互影响。
额外想吐槽一下,SmartAssembly 的公司 Red Gate 一定不喜好这款软件,由于界面做成下面这样竟然还长期不更新:
而且,如果要成功编译,还得用上同为 Red Gate 家出品的 SQL Server,如果不装,软件到处弹窗报错。只是报告缺点而已,干嘛还要开拓者装一个那么重量级的 SQL Server 啊!
详见:Why is SQL Server required — Redgate forums。
SmartAssembly 实质上是保护运用程序不被逆向或恶意修改。目前我利用的版本是 6,它供应了对 .NET Framework 程序的多种保护办法:
强署名 Strong Name Signing 强署名可以确保程序之间的依赖关系是严格确定的,如果对个中的一个依赖进行修改,将导致无法加载精确的程序集。微软供应了强署名工具,以是可以无需利用 SmartAssembly 的: Sn.exe (Strong Name Tool) - Microsoft DocsHow to: Sign an Assembly with a Strong Name - Microsoft Docs自动缺点上报 Automated Error Reporting SmartAssembly 会自动向 exe 程序注入非常捕获与上报的逻辑。功能利用率上报 Feature Usage Reporting SmartAssembly 会修正每个方法,记录这些方法的调用次数并上报。依赖合并 Dependencies Merging SmartAssembly 会将程序集中你勾选的的依赖与此程序凑集并成一个整的程序集。依赖嵌入 Dependencies Embedding SmartAssembly 会将依赖以加密并压缩的办法嵌入到程序集中,运行时进行解压缩与解密。实在这只是方便了支配(一个 exe 就能发给别人),并不能真正保护程序集,由于实际运行时还是解压并解密出来了。裁剪 Pruning SmartAssembly 会将没有用到的字段、属性、方法、事宜等删除。它声称删除了这些就能让程序逆向后代码更难读懂。名称稠浊 Obfuscation 修正类型、字段、属性、方法等的名称。流程稠浊 Control Flow Obfuscation 修正方法内的实行逻辑,使其实行错综繁芜。动态代理 References Dynamic Proxy SmartAssembly 会将方法的调用转到动态代理上。资源压缩加密 Resources Compression and Encryption SmartAssembly 会将资源以加密并压缩的办法嵌入到程序集中,运行时进行解压缩与解密。字符串压缩加密 Strings Encoding SmartAssembly 会将字符串都进行加密,运行时自动对其进行解密。防止 MSIL Disassembler 对其进行反编译 MSIL Disassembler Protection 在程序集中加一个 Attribute,这样 MSIL Disassembler 就不会反编译这个程序集。密封类 如果 SmartAssembly 创造一个类可以被密封,就会把它密封,这样能得到一点点性能提升。天生调试信息 Generate Debugging Information 可以天生稠浊后的 pdb 文件以上所有 SmartAssembly 对程序集的修正中,我标为 粗体 的是真的在做稠浊,而标为 斜体 的是一些赞助功能。
后面我只会解释其稠浊功能。
裁剪 Pruning我故意在 Whitman.Core 中写了一个没有被用到的 internal 类 UnusedClass,如果我们开启了裁剪,那么这个类将消逝。
▲ 没用到的类将消逝
特殊把稳,如果标记了 InternalsVisibleTo,尤其把稳不要欠妥心被误删了。
名称稠浊 Obfuscation类/方法名与字段名的稠浊名称稠浊中,类名和方法名的稠浊有三个不同级别:
等级 1 是利用 ASCII 字符集等级 2 是利用不可见的 Unicode 字符集等级 3 是利用高等重命名算法的不可见的 Unicode 字符集须要把稳:对付部分程序集,类与方法名(NameMangling)的等级只能选为 3,否则稠浊程序会无法完成编译。
字段名的稠浊有三个不同级别:
等级 1 是源码中字段名称和稠浊后字段名称逐一对应等级 2 是在一个类中的不同字段利用不同名称即可(这不废话吗,不过 SmartAssembly 该当是为了强调与等级 1 和等级 3 的不同,必须写一个描述)等级 3 是许可不同类中的字段利用相同的名字(这样能够更加让人难以理解)须要把稳:对付部分程序集,字段名(FieldsNameMangling)的等级只能选为 2 或 3,否则稠浊程序会无法完成编译。
实际试验中,以上各种组合常常会涌现无法编译的情形。
下面是 Whitman 中 RandomIdentifier 类中的部分字段在稠浊后的效果:
// Token: 0x04000001 RID: 1[CompilerGenerated][DebuggerBrowsable(DebuggerBrowsableState.Never)]private int \u0001;// Token: 0x04000002 RID: 2private readonly Random \u0001 = new Random();// Token: 0x04000003 RID: 3private static readonly Dictionary<int, int> \u0001 = new Dictionary<int, int>();12345678910
这部分的原始代码可以在 冷算法:自动天生代码标识符(类名、方法名、变量名) 找到。
如果你须要在稠浊时利用名称稠浊,你只须要在以上两者的组合中找到一个能够编译通过的组合即可,不须要特殊在意等级 1~3 的差异,由于实际上都做了稠浊,1~3 的差异对逆向来说难度差异非常小的。
须要 特殊小心如果有 InternalsVisibleTo 或者依据名称的反射调用,这种稠浊下极有可能挂掉!
!
!
请充分测试你的软件,牢记!
!
!
如果开启了 ChangeMethodParent,那么稠浊可能会将一个类中的方法转移到另一个类中,这使得逆向时对类型含义的解读更加匪夷所思。
打消特定的命名空间如果你的程序集中确实存在须要被按照名称反射调用的类型,或者有 internal 的类/方法须要被友元程序集调用,请打消这些命名空间。
流程稠浊 Control Flow Obfuscation列举我在 Whitman.Core 中的方法:
public string Generate(bool pascal){ var builder = new StringBuilder(); var wordCount = WordCount <= 0 ? 4 - (int) Math.Sqrt(_random.Next(0, 9)) : WordCount; for (var i = 0; i < wordCount; i++) { var syllableCount = 4 - (int) Math.Sqrt(_random.Next(0, 16)); syllableCount = SyllableMapping[syllableCount]; for (var j = 0; j < syllableCount; j++) { var consonant = Consonants[_random.Next(Consonants.Count)]; var vowel = Vowels[_random.Next(Vowels.Count)]; if ((pascal || i != 0) && j == 0) { consonant = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(consonant); } builder.Append(consonant); builder.Append(vowel); } } return builder.ToString();}123456789101112131415161718192021222324
▲ 这个方法可以在 冷算法:自动天生代码标识符(类名、方法名、变量名) 找到。
流程稠浊修正方法内部的实现。为了理解各种不同的流程稠浊级别对代码的影响,我为每一个稠浊级别都进行反编译查看。
▲ 没有稠浊
0 级流程稠浊
▲ 0 级流程稠浊
1 级流程稠浊
▲ 1 级流程稠浊
可以创造 0 和 1 实在完备一样。又被 SmartAssembly 耍了。
2 级流程稠浊2 级流程稠浊代码很长,以是我没有贴图:
// Token: 0x06000004 RID: 4 RVA: 0x00002070 File Offset: 0x00000270public string Generate(bool pascal){ StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2; if (-1 != 0) { stringBuilder2 = stringBuilder; } int num2; int num = num2 = this.WordCount; int num4; int num3 = num4 = 0; int num6; int num8; if (num3 == 0) { int num5 = (num <= num3) ? (4 - (int)Math.Sqrt((double)this._random.Next(0, 9))) : this.WordCount; if (true) { num6 = num5; } int num7 = 0; if (!false) { num8 = num7; } if (false) { goto IL_10E; } if (7 != 0) { goto IL_134; } goto IL_8E; } IL_6C: int num9 = num2 - num4; int num10; if (!false) { num10 = num9; } int num11 = RandomIdentifier.SyllableMapping[num10]; if (6 != 0) { num10 = num11; } IL_86: int num12 = 0; int num13; if (!false) { num13 = num12; } IL_8E: goto IL_11E; IL_10E: string value; stringBuilder2.Append(value); num13++; IL_11E: string text; bool flag; if (!false) { if (num13 >= num10) { num8++; goto IL_134; } text = RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; value = RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; flag = ((pascal || num8 != 0) && num13 == 0); } if (flag) { text = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } if (!false) { stringBuilder2.Append(text); goto IL_10E; } goto IL_86; IL_134: if (num8 >= num6) { return stringBuilder2.ToString(); } num2 = 4; num4 = (int)Math.Sqrt((double)this._random.Next(0, 16)); goto IL_6C;}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
▲ 2 级流程稠浊
这时就创造代码的可读性降落了,须要耐心才能解读其含义。
3 级流程稠浊以下是 3 级流程稠浊:
// Token: 0x06000004 RID: 4 RVA: 0x0000207C File Offset: 0x0000027Cpublic string Generate(bool pascal){ StringBuilder stringBuilder = new StringBuilder(); int num2; int num = num2 = this.WordCount; int num4; int num3 = num4 = 0; int num7; int num8; string result; if (num3 == 0) { int num5; if (num > num3) { num5 = this.WordCount; } else { int num6 = num5 = 4; if (num6 != 0) { num5 = num6 - (int)Math.Sqrt((double)this._random.Next(0, 9)); } } num7 = num5; num8 = 0; if (false) { goto IL_104; } if (7 == 0) { goto IL_84; } if (!false) { goto IL_12A; } return result; } IL_73: int num9 = num2 - num4; num9 = RandomIdentifier.SyllableMapping[num9]; IL_81: int num10 = 0; IL_84: goto IL_114; IL_104: string value; stringBuilder.Append(value); num10++; IL_114: string text; bool flag; if (!false) { if (num10 >= num9) { num8++; goto IL_12A; } text = RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; value = RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; flag = ((pascal || num8 != 0) && num10 == 0); } if (flag) { text = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } if (!false) { stringBuilder.Append(text); goto IL_104; } goto IL_81; IL_12A: if (num8 < num7) { num2 = 4; num4 = (int)Math.Sqrt((double)this._random.Next(0, 16)); goto IL_73; } result = stringBuilder.ToString(); return result;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
▲ 3 级流程稠浊
3 级流程稠浊并没有比 2 级高多少,可读性差不多。不过须要把稳的是,这些差异并不是随机差异,由于重复天生得到的流程结果是相同的。
4 级流程稠浊以下是 4 级流程稠浊:
// Token: 0x06000004 RID: 4 RVA: 0x0000207C File Offset: 0x0000027Cpublic unsafe string Generate(bool pascal){ void ptr = stackalloc byte[14]; StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2; if (!false) { stringBuilder2 = stringBuilder; } int num = (this.WordCount <= 0) ? (4 - (int)Math.Sqrt((double)this._random.Next(0, 9))) : this.WordCount; (int)ptr = 0; for (;;) { ((byte)ptr)[13] = (((int)ptr < num) ? 1 : 0); if ((sbyte)((byte)ptr + 13) == 0) { break; } (int)((byte)ptr + 4) = 4 - (int)Math.Sqrt((double)this._random.Next(0, 16)); (int)((byte)ptr + 4) = RandomIdentifier.SyllableMapping[(int)((byte)ptr + 4)]; (int)((byte)ptr + 8) = 0; for (;;) { ((byte)ptr)[12] = (((int)((byte)ptr + 8) < (int)((byte)ptr + 4)) ? 1 : 0); if ((sbyte)((byte)ptr + 12) == 0) { break; } string text = RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; string value = RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; bool flag = (pascal || (int)ptr != 0) && (int)((byte)ptr + 8) == 0; if (flag) { text = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } stringBuilder2.Append(text); stringBuilder2.Append(value); (int)((byte)ptr + 8) = (int)((byte)ptr + 8) + 1; } (int)ptr = (int)ptr + 1; } return stringBuilder2.ToString();}1234567891011121314151617181920212223242526272829303132333435363738394041424344
▲ 4 级流程稠浊
我们创造,4 级已经开始利用没有含义的指针来转换我们的内部实现了。这时除了外部调用以外,代码基本已无法解读其含义了。
动态代理 References Dynamic Proxy还是以上一节中我们 Generate 方法作为示例,在开启了动态代理之后(仅开启动态代理,其他都关掉),方法变成了下面这样:
// Token: 0x06000004 RID: 4 RVA: 0x0000206C File Offset: 0x0000026Cpublic string Generate(bool pascal){ StringBuilder stringBuilder = new StringBuilder(); int num = (this.WordCount <= 0) ? (4 - (int)\u0002.\u0002((double)\u0001.~\u0001(this._random, 0, 9))) : this.WordCount; for (int i = 0; i < num; i++) { int num2 = 4 - (int)\u0002.\u0002((double)\u0001.~\u0001(this._random, 0, 16)); num2 = RandomIdentifier.SyllableMapping[num2]; for (int j = 0; j < num2; j++) { string text = RandomIdentifier.Consonants[\u0003.~\u0003(this._random, RandomIdentifier.Consonants.Count)]; string text2 = RandomIdentifier.Vowels[\u0003.~\u0003(this._random, RandomIdentifier.Vowels.Count)]; bool flag = (pascal || i != 0) && j == 0; if (flag) { text = \u0006.~\u0006(\u0005.~\u0005(\u0004.\u0004()), text); } \u0007.~\u0007(stringBuilder, text); \u0007.~\u0007(stringBuilder, text2); } } return \u0008.~\u0008(stringBuilder);}123456789101112131415161718192021222324
▲ 动态代理
把稳到 _random.Next(0, 9) 变成了 \u0001.~\u0001(this._random, 0, 9),Math.Sqrt(num) 变成了 \u0002.\u0002(num)。
也便是说,一些常规方法的调用被更换成了一个代理类的调用。那么代理类在哪里呢?
▲ 天生的代理类
天生的代理类都在根命名空间下。比如刚刚的 \u0001.~\u0001 调用,便是下面这个代理类:
// Token: 0x0200001A RID: 26internal sealed class \u0001 : MulticastDelegate{ // Token: 0x06000030 RID: 48 public extern \u0001(object, IntPtr); // Token: 0x06000031 RID: 49 public extern int Invoke(object, int, int); // Token: 0x06000032 RID: 50 RVA: 0x000030A8 File Offset: 0x000012A8 static \u0001() { MemberRefsProxy.CreateMemberRefsDelegates(25); } // Token: 0x04000016 RID: 22 internal static \u0001 \u0001; // Token: 0x04000017 RID: 23 internal static \u0001 ~\u0001;}123456789101112131415161718192021字符串编码与加密 Strings Encoding字符串统一网络编码 Encode
字符串编码将程序集中的字符串都统一网络起来,存为一个资源;然后供应一个赞助类统一获取这些字符串。
比如 Whitman.Core 中的字符串现在被统一网络了:
▲ 统一网络的字符串和解密赞助类
在我的项目中,统一网络的字符串可以形成下面这份字符串(也即是上图中 Resources 文件夹中的那个文件内容):
cQ==dw==cg==dA==eQ==cA==cw==ZA==Zg==Zw==aA==ag==aw==bA==eg==eA==Yw==dg==Yg==bg==bQ==dHI=ZHI=Y2g=d2g=c3Q=YQ==ZQ==aQ==bw==dQ==YXI=YXM=YWk=YWlyYXk=YWw=YWxsYXc=ZWU=ZWE=ZWFyZW0=ZXI=ZWw=ZXJlaXM=aXI=b3U=b3I=b28=b3c=dXI=MjAxOC0wOC0yNlQxODoxMDo0Mw==`VGhpcyBhc3NlbWJseSBoYXMgYmVlbiBidWlsdCB3aXRoIFNtYXJ0QXNzZW1ibHkgezB9LCB3aGljaCBoYXMgZXhwaXJlZC4=RXZhbHVhdGlvbiBWZXJzaW9uxVGhpcyBhc3NlbWJseSBoYXMgYmVlbiBidWlsdCB3aXRoIFNtYXJ0QXNzZW1ibHkgezB9LCBhbmQgdGhlcmVmb3JlIGNhbm5vdCBiZSBkaXN0cmlidXRlZC4=IA==Ni4xMi41Ljc5OQ==U21hcnRBc3NlbWJseQ==UGF0aA==U29mdHdhcmVcUmVkIEdhdGVc(U29mdHdhcmVcV293NjQzMk5vZGVcUmVkIEdhdGVc123456789
虽然字符串难以读懂,但实在我原来便是这么写的;给你看看我的原始代码就知道了(来自 冷算法:自动天生代码标识符(类名、方法名、变量名)):
private static readonly List<string> Consonants = new List<string>{ \"大众q\公众,\"大众w\"大众,\"大众r\公众,\公众t\"大众,\公众y\"大众,\"大众p\"大众,\公众s\公众,\公众d\公众,\"大众f\"大众,\"大众g\"大众,\公众h\公众,\"大众j\公众,\公众k\"大众,\公众l\"大众,\公众z\"大众,\"大众x\"大众,\"大众c\公众,\公众v\"大众,\"大众b\"大众,\"大众n\"大众,\"大众m\公众, \"大众w\"大众,\"大众r\"大众,\"大众t\公众,\"大众p\公众,\"大众s\"大众,\公众d\"大众,\"大众f\"大众,\"大众g\"大众,\"大众h\"大众,\公众j\公众,\公众k\"大众,\公众l\"大众,\"大众c\"大众,\公众b\公众,\"大众n\公众,\"大众m\公众, \"大众r\公众,\公众t\"大众,\"大众p\"大众,\"大众s\公众,\"大众d\"大众,\"大众h\公众,\"大众j\"大众,\"大众k\"大众,\"大众l\"大众,\"大众c\公众,\"大众b\"大众,\"大众n\"大众,\公众m\"大众, \"大众r\公众,\"大众t\"大众,\公众s\"大众,\公众j\"大众,\"大众c\"大众,\"大众n\"大众,\"大众m\公众, \公众tr\公众,\"大众dr\公众,\"大众ch\"大众,\公众wh\"大众,\"大众st\"大众, \"大众s\"大众,\公众s\"大众};123456789
天生的字符串获取赞助类就像下面这样不太随意马虎读懂:
// Token: 0x0200000A RID: 10public class Strings{ // Token: 0x0600001C RID: 28 RVA: 0x00002B94 File Offset: 0x00000D94 public static string Get(int stringID) { stringID -= Strings.offset; if (Strings.cacheStrings) { object obj = Strings.hashtableLock; lock (obj) { string text; Strings.hashtable.TryGetValue(stringID, out text); if (text != null) { return text; } } } int index = stringID; int num = (int)Strings.bytes[index++]; int num2; if ((num & 128) == 0) { num2 = num; if (num2 == 0) { return string.Empty; } } else if ((num & 64) == 0) { num2 = ((num & 63) << 8) + (int)Strings.bytes[index++]; } else { num2 = ((num & 31) << 24) + ((int)Strings.bytes[index++] << 16) + ((int)Strings.bytes[index++] << 8) + (int)Strings.bytes[index++]; } string result; try { byte[] array = Convert.FromBase64String(Encoding.UTF8.GetString(Strings.bytes, index, num2)); string text2 = string.Intern(Encoding.UTF8.GetString(array, 0, array.Length)); if (Strings.cacheStrings) { try { object obj = Strings.hashtableLock; lock (obj) { Strings.hashtable.Add(stringID, text2); } } catch { } } result = text2; } catch { result = null; } return result; } // Token: 0x0600001D RID: 29 RVA: 0x00002CF4 File Offset: 0x00000EF4 static Strings() { if (Strings.MustUseCache == \公众1\"大众) { Strings.cacheStrings = true; Strings.hashtable = new Dictionary<int, string>(); } Strings.offset = Convert.ToInt32(Strings.OffsetValue); using (Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(\公众{f6b5a51a-b2fb-4143-af01-e2295062799f}\"大众)) { int num = Convert.ToInt32(manifestResourceStream.Length); Strings.bytes = new byte[num]; manifestResourceStream.Read(Strings.bytes, 0, num); manifestResourceStream.Close(); } } // Token: 0x0400000C RID: 12 private static readonly string MustUseCache = \"大众0\"大众; // Token: 0x0400000D RID: 13 private static readonly string OffsetValue = \"大众203\"大众; // Token: 0x0400000E RID: 14 private static readonly byte[] bytes = null; // Token: 0x0400000F RID: 15 private static readonly Dictionary<int, string> hashtable; // Token: 0x04000010 RID: 16 private static readonly object hashtableLock = new object(); // Token: 0x04000011 RID: 17 private static readonly bool cacheStrings = false; // Token: 0x04000012 RID: 18 private static readonly int offset = 0;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
天生字符串获取赞助类后,原来写着字符串的地方就会被更换为 Strings.Get(int) 方法的调用。
字符串压缩加密 Compress前面那份统一网络的字符串依然还是明文存储为资源,但还可以进行压缩。这时,Resources 中的那份字符串资源现在是二进制文件(截取前 256 字节):
00000000: 7b7a 7d02 efbf bdef bfbd 4def bfbd efbf00000010: bd7e 6416 efbf bd6a efbf bd22 efbf bd0800000020: efbf bdef bfbd 4c42 7138 72ef bfbd efbf00000030: bd54 1337 efbf bd0e 22ef bfbd 69ef bfbd00000040: 613d efbf bd6e efbf bd35 efbf bd0a efbf00000050: bd33 6043 efbf bd26 59ef bfbd 5471 efbf00000060: bdef bfbd 2cef bfbd 18ef bfbd 6def bfbd00000070: efbf bdef bfbd 64ef bfbd c9af efbf bdef00000080: bfbd efbf bd4b efbf bdef bfbd 66ef bfbd00000090: 1e70 efbf bdef bfbd ce91 71ef bfbd 1d5e000000a0: 1863 efbf bd16 0473 25ef bfbd 2204 efbf000000b0: bdef bfbd 11ef bfbd 4fef bfbd 265a 375f000000c0: 7bef bfbd 19ef bfbd d5bd efbf bdef bfbd000000d0: efbf bd70 71ef bfbd efbf bd05 c789 efbf000000e0: bd51 eaae beef bfbd ee97 adef bfbd 0a33000000f0: d986 141c 2bef bfbd efbf bdef bfbd 1fef12345678910111213141516
这份压缩的字符串在程序启动的时候会进行一次解压,随后就直接读取解压后的字符串了。以是会占用启动韶光(虽然不长),但不会占用太多运行时时间。
为了能够解压出这些压缩的字符串,Strings 类比较于之前会在读取后进行一次解压缩(解密)。可以看下面我额外标注出的 Strings 类新增的一行。
using (Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(\公众{4f639d09-ce0f-4092-b0c7-b56c205d48fd}\"大众)) { int num = Convert.ToInt32(manifestResourceStream.Length); byte[] buffer = new byte[num]; manifestResourceStream.Read(buffer, 0, num);++ Strings.bytes = SimpleZip.Unzip(buffer); manifestResourceStream.Close(); }12345678
至于嵌入个中的解压与解密类 SimpleZip,我就不能贴出来了,由于反编译出来有 3000+ 行:
字符串缓存 UseCache
与其他的缓存策略一样,每次获取字符串都太花费打算资源的话,就可以拿内存空间进行缓存。
在实际稠浊中,我创造无论我是否开启了字符串缓存,实际 Strings.Get 方法都会缓存字符串。你可以回到上面去重新阅读 Strings.Get 方法的代码,创造其本来就已带缓存。这可能是 SmartAssembly 的 Bug。
利用类的内部委托获取字符串 UseImprovedEncoding之前的稠浊都会在原来有字符串地方利用 Strings.Get 来获取字符串。而如果开启了这一选项,那么 Strings.Get 就不是全局调用的了,而是在类的内部调用一个委托字段。
比如从 Strings.Get 调用修正为 \u0010(),,而 \u0010 是我们自己的类 RandomIdentifier 内部的被额外加进去的一个字段 internal static GetString \u0010;。
防止 MSIL Disassembler 对其进行反编译 MSIL Disassembler Protection这实在是个没啥用的选项,由于我们程序集只会多出一个全局的特性:
[assembly: SuppressIldasm]1
只有 MSIL Disassembler 和基于 MSIL Disassembler 的工具认这个特性。真正想逆向程序集的,根本不会在乎 MSIL Disassembler 被禁掉。
dnSpy 和 dotPeek 实际上都忽略了这个特性,依然能毫无障碍地反编译。
dnSpy 可以做挺多事儿的,比如:
断点调试 Windows 源代码 - lindexi神器如 dnSpy,无需源码也能修正 .NET 程序 - walterlv密封在 OtherOptimizations 选项中,有一项 SealClasses 可以将所有可以密封的类进行密封(当然,此操作不会修正 API)。
在上面的例子中,由于 RandomIdentifier 是公有类,可能被继续,以是只有预先写的内部的 UnusedClass 被其标记为密封了。
// Token: 0x02000003 RID: 3internal sealed class UnusedClass{ // Token: 0x06000007 RID: 7 RVA: 0x000026D0 File Offset: 0x000008D0 internal void Run() { } // Token: 0x06000008 RID: 8 RVA: 0x000026D4 File Offset: 0x000008D4 internal async Task RunAsync() { }}12345678910111213实际项目中,我该如何选择
既然你希望选择“稠浊”,那么你肯定是希望能进行最大程度的保护。在担保你没有额外产生 Bug,性能没有明显丢失的情形下,能稠浊得多厉害就稠浊得多厉害。
基于这一原则,我推举的稠浊方案有(按推举顺序排序):
流程稠浊 建议必选直接选用 4 级流程(不屈安代码)稠浊,如果出问题才换为 3 级(goto)稠浊,理论上不须要利用更低级别流程稠浊对性能的影响是非常小的,由于多实行的代码都是有编译期级别优化的,没有太多性能开销的代码流程稠浊仅影响实现,不修正 API,以是基本不会影响其他程序各种对此程序集的调用名称稠浊 只管即便选择任意选择类/方法名和字段名的级别,只要能编译通过就行(由于无论选哪个,对程序的影响都一样,逆向的难度差异也较小)名称稠浊不影响程序实行性能,以是只要能打开,就只管即便打开如果有 InternalsVisibleTo 或者可能被其他程序集按名称反射调用,请: 关闭此稠浊利用 Exclude 打消特定命名空间,使此命名空间下的类/方法名不进行名称稠浊如果你能接管用 Attribute 标记某些类不应该稠浊类名,也可以利用这些标记(只是我不推举这么做,这让稠浊污染了自己的代码)动态代理 推举选择动态代理仅影响实现,不修正 API,以是基本不会影响其他程序各种对此程序集的调用动态代理会天生新的类/委托来更换之前的方法调用,以是可能造成非常轻微的性能丢失(一样平常可以忽略)字符串压缩加密 可以选择由于所有的字符串都被统一成一个资源,如果额外进行压缩加密,那么逆向时理解程序的含义将变得非常困难(没有可以参考的锚点)会对启动韶光有轻微的性能影响,如果额外压缩加密,那么会有更多性能影响;如果你对启念头能哀求较高,还是不要选了会轻微增加内存占用和读取字符串时的 CPU 占用,如果你对程序性能哀求非常高,还是不要选了以上四种稠浊办法从四个不同的维度对你类与方法的实现进行了稠浊,使得你写的类的任何地方都变得无法辨认。流程稠浊修正方法内实现的逻辑,名称稠浊修正类/属性/方法的名称,动态代理将方法内对其他方法的调用变得不再直接,字符串压缩加密将使得字符串不再具有可读的含义。对逆向阅读影响最大的便是以上 4 种稠浊了,如果可能,建议都选择开启。
如果你的程序中有须要保护的“嵌入的资源”,在没有自己的保护手段的情形下,可以利用“资源压缩加密”。不过,我更加推举你自己进行加密。
至于 SmartAssembly 推举的其他选项,都是噱头重于实际效果:
裁剪 一样平常也不会有多少开拓者会故意往程序集中写一些不会用到的类吧!依赖合并/依赖嵌入 并不会对逆向造成障碍,开不开启差别不大,反而降落了性能防止 MSIL Disassembler 反编译 并不会对逆向造成障碍,防君子不防小人密封类 声称可以提升性能,但这点性能提升微乎其微
SmartAssembly 的官方文档写得还是太大略了,很难得到每一个设置项的含义和实际效果。
以上这些信息的得出,离不开 dnSpy 的反编译。