.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) 運轉主程序。
這種加殼辦法的兩個要害點,一個是主程序作爲殼程序的資本文件存在,第二個是先對資本文件解密然後再反射履行。