.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的构造
   ForCompress.exe的构造
  
  下面咱们发动一款.NET压缩工具,NETZ来对ForCompress.exe进行加壳。加壳以后,咱们再次发动Reflector来检查加壳的文件。如图2所示。
  加壳以后的ForCompress.exe文件
   加壳以后的ForCompress.exe文件
  比照图1和图2,咱们发现称号空间ForCompress变成了netz,类Progress变成了NetzStartter。程序集多多了个资本文件app.resources。下面咱们打开NetzStartter类,来检查其下的办法。如图3所示。
  NetzStartter类的办法
  图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 < files.Length; 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 < xrRm.Count; 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) 运转主程序。
  这种加壳办法的两个要害点,一个是主程序作为壳程序的资本文件存在,第二个是先对资本文件解密然后再反射履行。