【标题】比较两款c#的本地代码加密软件
――Remotesoft Protector和MaxtoCode
【作者】henryouly
【声明】本文纯粹技术探讨性质,转载时请保留作者信息。本人才疏学浅,最近才初步接触破解和.NET平台,对当前的最新技术了解也不充分,分析难免有失偏颇,请大家指教
上几篇文章对C#的加密技术作了详细探讨,其中主要研究了混淆器保护.NET程序的一般原理以及破解方法。显然IL语言本身的特点使得单纯在IL层面上做的保护十分苍白无力。于是另外一类软件保护的产品产生了――本地代码编译。
Remotesoft Protector是国外某软件公司的加密产品,MaxtoCode则是国人Jason.NET的力作。这两款产品共有的特点都是,a)需要带一个本地编译的dll文件发布,b)功能代码不再在类里面直接实现,用任何IL反汇编器看到的都是空的方法。如图
[函数体哪里去了?]
当然,程序执行的时候,函数体还是要被变戏法一样把内容填充回去的。下来我们具体研究到底这类保护软件耍了什么把戏。
先看看Remotesoft Protecter加密过的WebGrid.NET 3.5。这个ASP.NET程序需要ISNet.WebUI.WebGrid.dll和rscoree.dll一起放到bin下才能运行。先用Reflector打开ISNet.WebUI.WebGrid.dll,留意到这些地方
internal class <PrivateImplementationDetails>
{
// Methods
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void $$method-1();
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void $$method-2();
[MethodImpl(MethodImplOptions.ForwardRef), DllImport("rscoree.dll", CharSet=CharSet.Ansi, ExactSpelling=true)]
private static extern void _RSEEStartup(int A_0);
[MethodImpl(MethodImplOptions.ForwardRef), DllImport("rscoree.dll", CharSet=CharSet.Ansi, ExactSpelling=true)]
private static extern void _RSEEUpdate(IntPtr A_0);
// Fields
private static bool $$started-1;
private static bool $$started-2;
}
[MethodImpl(MethodImplOptions.NoInlining)]
internal static unsafe void $$method-1()
{
if (!<PrivateImplementationDetails>.$$started-1)//保证初始化只进行一次
{
fixed (char* local1 = "")
{
<PrivateImplementationDetails>._RSEEStartup((int) local1);
//这里用到一个小技巧,通过local1来得到代码段在内存中的RVA,传给_RSEEStartup
}
<PrivateImplementationDetails>.$$started-1 = true;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void $$method-2()
//注:这个类原来是混淆过的(而且非流程混淆,是构造特殊堆栈结构来阻止Reflector解释成C#语句
//是Remotesoft Protector作者有意手动编写的,用前几篇文章介绍的方法反混淆出C#代码
{
if (!<PrivateImplementationDetails>.$$started-2) //保证初始化只进行一次
{
StackTrace trace1 = new StackTrace();
if (trace1.FrameCount > 2)
{
<PrivateImplementationDetails>._RSEEUpdate(trace1.GetFrame(2).GetMethod().MethodHandle.Value);
//获得调用栈中的方法名字,作为参数传递给_RSEEUpdate,稍后解释为什么
<PrivateImplementationDetails>.$$started-2 = true;
}
}
}
另外一个特别的地方是,我们留意到不少类当中都加入了一个静态构造函数,内容均一样,如下:
[MethodImpl(MethodImplOptions.NoInlining)]
static WebGrid()
{
<PrivateImplementationDetails>.$$method-1();
<PrivateImplementationDetails>.$$method-2();
}
这个构造函数仅仅在该类第一次被使用的时候调用。作用就是把该类被抽走的部分回填到内存中的assembly当中。
看到这里已经很明显了,每个类在第一次使用前,静态构造函数首先被调用,用于调用method-1()和method-2(),method-1的作用是定位当前的代码在内存中的位置,而method-2中的GetFrame(2)是取出该类(比如WebGrid)的构造函数的句柄。这两个功能联合起来即可实现类似Win32 PE文件的stolen byte的工作原理,在使用类以前修改构造函数,利用构造函数来把其他代码填回去。
我稍微尝试了一下,发现把内存中的assembly提取出来并不容易。.NET框架没有提供直接访问内存中的IL code的办法,所以不可能得到函数体。而尝试用LordPE把内存dump出来,发现函数体内依然是空的,显然Remotesoft并不直接填充该内存区域,而是采用类似ResolveHandler的办法来实现动态加入的代码和原Assembly的链接。
另外,通过查看rscoree.dll和ISNet.WebUI.WebGrid.dll,发现两者修改日期一样,相信函数功能是被静态编译到rscoree.dll当中去。ISNet.WebUI.WebGrid.dll纯粹是一个不包含任何功能的躯壳。
再来分析一下MaxtoCode的原理。上网下载了MaxtoCode 2.0试用版。MaxtoCode的下载页面说明运行依赖于ilasm和ildasm,明显感觉到MaxtoCode需要先把assembly文件dasm成IL文件,然后加入自己的功能,再重新组装为assembly。
简单分析了一下,MaxtoCode的构造原理应该为:
1、 调用ildasm把assembly文件反编译成IL代码
2、 清除IL文件中所有注释
3、 在IL文件中增加一个新的entry point
.method private hidebysig static void
_1() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
.maxstack 5
.locals init (string Jason_0)
IL_0000: ldsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_0007: ldc.i4.0
IL_0008: bne.un.s IL_0021
IL_000a: call class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::GetExecutingAssembly()
IL_000f: callvirt instance string [mscorlib]System.Reflection.Assembly::get_Location()
IL_0014: stloc.s Jason_0
IL_0015: ldloca.s Jason_0
IL_0017: call int32 'Reflector'.'Application'::AABBCCDDEE12345(string&)
IL_001c: stsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_0021: ldsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_0026: call bool 'Reflector'.'Application'::JasonIsGood_Actions(int32)
IL_0028: pop
IL_0029: ldsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_002d: ldc.i4 0x86da
IL_0032: ldc.i4.2
IL_0036: ldc.i4.0
IL_0037: ldc.i4.1
IL_0038: call bool 'Reflector'.'Application'::JasonIsGood_Actions_1(int32,
int32,
int32,
int32,
int32)
IL_003d: pop
IL_0042: call void 'Reflector'.'Application'::MTC___000086DA()
IL_0043: ret
}
4、 原entry point改名为MTC___000086DA,并增加如下内容
.method private hidebysig static void
MTC___000086DA() cil managed
{
.locals init (class Reflector.Application V_0,string Jason_0)
IL_0000: ldsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_0007: ldc.i4.0
IL_0008: bne.un.s IL_0021
IL_000a: call class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::GetExecutingAssembly()
IL_000f: callvirt instance string [mscorlib]System.Reflection.Assembly::get_Location()
IL_0014: stloc.s Jason_0
IL_0015: ldloca.s Jason_0
IL_0017: call int32 'Reflector'.'Application'::AABBCCDDEE12345(string&)
IL_001c: stsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_0021: nop
IL_0028: ldsfld int32 'Reflector'.'Application'::Locate__Assembly__Images__1
IL_002d: ldc.i4 0x86da
IL_0032: ldc.i4.2
IL_0036: ldc.i4.0
IL_0037: ldc.i4.0
IL_0038: call bool 'Reflector'.'Application'::JasonIsGood_Actions_1(int32,
int32,
int32,
int32,
int32)
IL_003d: stsfld bool 'Reflector'.'Application'::MTC___000086DA_field
……Main原来的内容……
5、 在待加密的类当中加入:
.field private static int32 Locate__Assembly__Images__1
.field private static bool Locate__Assembly__Images__2
.method private hidebysig static pinvokeimpl("kernel32" as "GetModuleHandleA" nomangle ansi lasterr winapi)
int32 'AABBCCDDEE12345'(string& marshal( byvalstr) 'lpModuleName') cil managed preservesig
{
}
.method private hidebysig static pinvokeimpl("MShare.dll" as "EC1DB9C1620C48588C4701045B242FA9" nomangle ansi lasterr winapi)
bool 'JasonIsGood_Actions'(int32 'a') cil managed preservesig
{
}
.method private hidebysig static pinvokeimpl("MShare.dll" as "F1B0C9B05CF2496c8873B60602A22743" nomangle ansi lasterr winapi)
bool 'JasonIsGood_Actions_1'(int32 'a',int32 'b',int32 'c',int32 'd',int32 'e') cil managed preservesig
{
}
6、 加入PrivateImplementationDetails,后面详细讲。
7、 混淆,把类名、变量名、方法名替换为不可见字符(如后面的’\r\n’)
8、 重新编译回去,并把安装目录下的Attick.dll 复制为MShare.dll
前5步是直接用Reflector的主程序所分析出来的,还记得Reflector会使得ildasm崩溃吗?哈哈,就是利用它的崩溃等待按确定的时间慢慢翻查临时文件。
因为Reflector会引起崩溃,不可能得到最后的exe文件,所以我又另外写了一个小demo文件用作测试。用Reflector查看加密后的结果:
internal class <PrivateImplementationDetails>
{
// Methods
[DllImport("kernel32", EntryPoint="GetModuleHandleA", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern int //这个函数名字是”\r\n”,下面的名字均是这种情况导致换行
([MarshalAs(UnmanagedType.VBByRefStr)] ref string lpModuleName);
[DllImport("MShare.dll", EntryPoint="EC1DB9C1620C48588C4701045B242FA9", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool //同上
(int a);
[DllImport("MShare.dll", EntryPoint="F1B0C9B05CF2496c8873B60602A22743", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool
(int a, int b, int c, int d, int e);
// Fields
private static bool
;
private static int
;
internal static $$struct0x6000001-1 $$method0x6000001-1 = { 0x35, 0x46, 0x42, 0x32, 0x38, 0x31, 0x46, 0x36 };
// Nested Types
[StructLayout(LayoutKind.Explicit, Size=8, Pack=1)]
private struct $$struct0x6000001-1
{
}
}
internal class Class1
{
// Methods
private static void
();
[DllImport("kernel32", EntryPoint="GetModuleHandleA", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern int
([MarshalAs(UnmanagedType.VBByRefStr)] ref string lpModuleName);
[DllImport("MShare.dll", EntryPoint="EC1DB9C1620C48588C4701045B242FA9", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool
(int a);
[DllImport("MShare.dll", EntryPoint="F1B0C9B05CF2496c8873B60602A22743", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool
(int a, int b, int c, int d, int e);
public Class1();
[STAThread]
private static void Main();
// Fields
private static bool
;
private static bool
;
private static int
;
}
[STAThread]
private static void Main()
{
if (Class1.
== 0)
{
Class1.
= Class1.
(ref Assembly.GetExecutingAssembly().Location);
}
Class1.
(Class1.
);
Class1.
(Class1.
, 0x2e, 2, 0, 1);
Class1.
();
}
其中method0x6000001-1应该就是加密后的函数体了。MaxtoCode思想上和Remotesoft是类似的,不过实现手法上有所不同。MaxtoCode的Mshare.dll文件是MaxtoCode作者预先写好的,不包含任何被加密软件的功能,仅仅是确定解密的算法并进行解密(据作者的介绍,企业版共有7种不同的加密算法或变体)。被抽取的功能(即stolen byte)是经过加密变换后用静态的方式直接保存在assembly当中。于是,MaxtoCode的加密强度在于分析出函数体的解密算法的困难性。
对这两种加密办法,目前我并未找到方便的破解办法(但并不代表一定没有,也许只是我不知道)。.NET框架并不提供把内存中的Assembly内容转换到本地exe/dll的办法。如果是.NET的exe文件,还可以通过OD进行调试(当然此时IL code已经经过CLR自动编译,变成本地代码了),如果是ASP.NET的dll文件,似乎就无法从exe调用进去,只能通过静态分析加密软件产生的对应dll文件。也许.NET的动态跟踪技术发展成熟后,就能出现破解这类本地代码编译的保护壳的通用办法。
【后记】
在JasonNET兄的提醒下,发现Remotesoft Protector把类的内容保存在rscoree.dll,这个说法太武断了。经过进一步分析,确实很有可能类的内容还是保存在原dll中,依据如下
1)查看ISNet.WebUI.WebGrid.dll的节区表,发现比正常编译出来的dll多了.rdata一节,大小为100多k
2)用ildasm反编译后,再用ilasm重新编译,文件大小明显减少,显然.NET的ildasm直接忽略处理.rdata的内容
3)查看rscoree.dll,所有模块间调用,发现ImageRvaToVa,ImageNtHeader,VirtualAlloc等API
4)__abstract.__abstract当中有不明含义的加密字段以及大量和程序结构有关的Hashtable
各种依据表明,被抽调函数内容很可能依然保存在IsNet.WebUI.WebGrid.dll当中,特别进行加入此后记,加以更正。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)