.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