.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 < 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) 运转主程序。
这种加壳办法的两个要害点,一个是主程序作为壳程序的资本文件存在,第二个是先对资本文件解密然后再反射履行。
转载于:https://blog.51cto.com/smallant/1541058