.NET程序加壳的基本原理和方式浅析 加壳程序是一种常用的保护应用程序的办法,确切的说是一种加密办法。取名为壳,意思是说这种对程序的保护办法就像植物种子的外壳,咱们运用一段程序将咱们的主程序包裹在其间,不能轻易被其他人看见。 被加壳的程序在运转时先要运转一段附加的指令,这段附加的指令完结有关操作后会发动主程序。 加壳的办法大致可分为压缩和加密。 传统的非保管程序,加壳的目标是汇编指令;对.NET程序的加壳目标则是元数据和IL代码。对.NET程序的加壳,在理论和办法上并没有啥创新,目前都是直接承继与Windows程序的加壳理论和办法。大多数.NET加壳工具也是传统的加壳工具在本身功能上供给了拓展。纯.NET完成的加壳工具仍是很少。加壳的办法许多,咱们这儿以常见的保管压缩壳为例进行解说。 为了探究其压缩原理,咱们先创立一段代码用于试验。 用于加壳程序源码: class Program { static void Main(string[] args) { DoSth(); } public static void DoSth() { } } 代码终究生成ForCompress.exe文件。运用Reflector检查其IL代码,。 Main办法的IL代码 .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 8 L_0000: nop L_0001: call void ForCompress.Program::DoSth() L_0006: nop L_0007: ret } 此刻,ForCompress.exe在Reflector中结果如图1所示。 ForCompress.exe的构造 下面咱们发动一款.NET压缩工具,NETZ来对ForCompress.exe进行加壳。加壳以后,咱们再次发动Reflector来检查加壳的文件。如图2所示。 加壳以后的ForCompress.exe文件 比照图1和图2,咱们发现称号空间ForCompress变成了netz,类Progress变成了NetzStartter。程序集多多了个资本文件app.resources。下面咱们打开NetzStartter类,来检查其下的办法。如图3所示。 图3 NetzStartter类的办法 从图3中咱们能够看出,NetzStartter类界说了一系列咱们"不认识"的办法,可是却没有代码的DoSth办法。下面咱们来剖析一下加壳以后的exe文件的发动进程。 首要定位到Main办法,检查其源代码,如代码清单9-16所示。 代码清单9-16 NetzStartter类的Main办法 [STAThread] public static int Main(string[] args) { try { InitXR(); AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(NetzStarter.NetzResolveEventHandler); return StartApp(args); } catch (Exception exception) { string str = " .NET Runtime: "; Log(string.Concat(new object[] { "#Error: ", exception.GetType().ToString(), Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace, Environment.NewLine, exception.InnerException, Environment.NewLine, "Using", str, Environment.Version.ToString(), Environment.NewLine, "Created with", str, "2.0.50727.4927" })); return -1; } } 代码清单9-16中的Main办法中,首要调用了InitXR办法,然后为AppDomain.CurrentDomain.AssemblyResolve事情添加处置办法,最终调用StartApp办法。咱们首要看看InitXR办法做了些啥事情。InitXR办法源码如代码清单9-17所示。 代码清单9-17 InitXR办法源码 private static void InitXR() { try { string str = @"file:\"; string str2 = "-netz.resources"; string directoryName = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); if (directoryName.StartsWith(str)) { directoryName = directoryName.Substring(str.Length, directoryName.Length - str.Length); } string[] files = Directory.GetFiles(directoryName, "*" + str2); if ((files != null) && (files.Length > 0)) { xrRm = new ArrayList(); for (int i = 0; i { string fileName = Path.GetFileName(files[i]); ResourceManager manager = ResourceManager.CreateFileBasedResourceManager(fileName.Substring(0, fileName.Length - str2.Length) + "-netz", directoryName, null); if (manager != null) { xrRm.Add(manager); } } } } catch { } } 代码清单9-17的代码很明晰,在特定的文件途径中搜索资本文件,然后添加到全局变量xrRm中。 Main办法中的AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(NetzStarter.NetzResolveEventHandler)一句代码也无需多言,仅仅指定程序集解析失败时的事情处置。 如今咱们看StartApp办法的源码,如代码清单9-18所示。 代码清单9-18 StartApp办法源码 public static int StartApp(string[] args) { byte[] resource = GetResource("A6C24BF5-3690-4982-887E-11E1B159B249"); if (resource == null) { throw new Exception("application data cannot be found"); } int num = InvokeApp(GetAssembly(resource), args); resource = null; return num; } StartApp办法,从姓名上看,应该是调用被加密的源程序。在办法体内,首要调用了GetResource办法,回来了指定的资本,然后调用InvokeApp办法进入主程序。为了弄清楚来龙去脉,咱们先看看GetResource办法终究做了啥?代码清单9-19是GetResource办法的源码。 代码清单9-19 GetResource办法源码 private static byte[] GetResource(string id) { byte[] buffer = null; if (rm == null) { rm = new ResourceManager("app", Assembly.GetExecutingAssembly()); } try { inResourceResolveFlag = true; string name = MangleDllName(id); if ((buffer == null) && (xrRm != null)) { for (int i = 0; i { try { ResourceManager manager = (ResourceManager) xrRm[i]; if (manager != null) { buffer = (byte[]) manager.GetObject(name); } } catch { } if (buffer != null) { break; } } } if (buffer == null) { buffer = (byte[]) rm.GetObject(name); } } finally { inResourceResolveFlag = false; } return buffer; } 如今咱们对代码清单9-19的代码做扼要的剖析。 if (rm == null) { rm = new ResourceManager("app", Assembly.GetExecutingAssembly()); } 上面这句代码从当时程序会集获取称号为app的资本文件,回到图9-20,咱们能够看到app. Resources文件是内嵌在程序会集的,能够被获取。接下来的代码获取指定称号的资本,然后以byte数组的方式回来。回来的资本的用处是啥呢?咱们持续剖析。 InvokeApp(GetAssembly(resource), args); 上面是StartApp办法最终的调用,GetAssembly办法,从姓名上看是获取程序集,其参数是GetResource办法回来的byte数组。咱们到它的源码中一探终究。GetAssembly办法的源码如代码清单9-20所示。 代码清单9-20 GetAssembly办法源码 private static Assembly GetAssembly(byte[] data) { MemoryStream stream = null; Assembly assembly = null; try { stream = UnZip(data); stream.Seek(0L, SeekOrigin.Begin); assembly = Assembly.Load(stream.ToArray()); } finally { if (stream != null) { stream.Close(); } stream = null; } return assembly; } 代码清单9-20的代码也很简单,从byte数组转化到程序集。这儿咱们唯一需求留意的当地是下面这句代码: stream = UnZip(data); UnZip办法对byte数组进行解压缩。这个办法是整个程序运转的最要害的办法,可是解压缩的详细完成咱们不去重视。如果您感兴趣的话能够自行研讨。 得到程序集以后,才真实的开端履行InvokeApp办法,咱们看代码清单9-21。 代码清单9-21 InvokeApp源码 private static int InvokeApp(Assembly assembly, string[] args) { MethodInfo entryPoint = assembly.EntryPoint; ParameterInfo[] parameters = entryPoint.GetParameters(); object[] objArray = null; if ((parameters != null) && (parameters.Length > 0)) { objArray = new object[] { args }; } object obj2 = entryPoint.Invoke(null, objArray); if ((obj2 != null) && (obj2 is int)) { return (int) obj2; } return 0; } 从代码清单9-21中咱们看到,这段代码首要获取程序集的入口函数,也即是Main办法,然后履行。到这儿,程序才真实的从外壳程序转到真实的主程序。 结合上面的剖析,咱们总结一下一个纯.NET压缩壳程序的运转流程: 1) 程序运转时首要运转外壳程序。 2) 外壳程序从其资本中读取主程序的原始数据。 3) 对原始数据解压缩,转化成程序集。 4) 运转主程序。 这种加壳办法的两个要害点,一个是主程序作为壳程序的资本文件存在,第二个是先对资本文件解密然后再反射履行。 09-20 16:04