由浅至深,谈谈.NET混淆原理 [转]
前段时间特别忙,没有时间更新自己的博客,也感到非常过意不去。可是我工作中的经历也许不是大家更感兴趣的话题,再加上framesniper兄把我拉进了 Inside IL and CLR 团队,虽嘴上说忙,但必须还要是做点贡献,所以正好赶上MaxtoCode 2.0 差不多快发布了,抽出几天时间,写写此领域的文章。
随便先说一下:凡是一个事物的存在,必然有存在的理由。有的朋友说:“你的代码没有价值,没有必要混淆,我承诺我是永远开源的”。对于这样的反驳我听了也不止一次,我觉得不能以个论全,开源是一件非常快乐的事情,可混淆器、加密器的存在是因为有这个需求。所以这样的话题是没有意义的,我也不会去争论。
呵呵,跑题了。。。
好,回到正题上,也许有很多人已经了解什么是混淆了,也知道混淆原理,不过我想应该有更多的人不知道,我们因为知道别人是怎么来处理混淆的,以及对混淆进行反向操作的,这样,我们才能更好的保护自己的知识产权。
我打算分为这么几个部分来试着谈谈.NET混淆原理
1. IL 基础,什么是IL
2. 最简单的混淆
3. 什么是流程混淆,它的利与弊
4. 反混淆实战 (原理 + 工具篇)
5. 新一代 .NET 代码保护加密工具 MaxtoCode 基本原理
6. 其它保护手段
好,今天我们来讲讲基础 ―― .NET 中的 IL
相信大家都知道不管你使用C#还是VB.NET还是C++ 托管,最后编译出来的都是IL语言程序集。
什么是IL呢,它是一种中间语言字节码,存在于高级语言和机器码的一种中间语言。它的作用就是建立“统一”运行的.NET运行环境,使net 可以跨平台 (不过,从实际情况来看,MS是不会允许net跨平台的,至少3年内不会,甚至更长。其实,跨平台也没什么好的,看看Java,号称一次编译,到运行,结果变成一次编译,到处调试!我就在Windows系统下没见过大量用Java编写的好工具,也许是偶不经常关注它的原因吧!!)。
不好意思,刚刚又跑题了,近来思想老打岔,唉,这是个不好的现象。IL 的格式与汇编语言的格式极为相似,所不同的是,IL语言比汇编语言更加易懂,因为它里面可以直接调用已封装好的Object,而且运行逻辑也与高级语言一致,所以基本上是差不多的。
我们不能对IL做一个系统的介绍,所以我们用一段非常简单的C#代码,让我们看看:(凡事从简单入手,熟悉后再开始复杂)
这段代码没有意义,我只是为了增加运算量,做强度测试的时候再这样写的。我们看看这段代码被译为 IL 将是什么模样。C#的代码对比一下,基本上还是比较清楚的,可能有的朋友已经被ldarg、starg、ldloc、stloc搞糊涂了,呵呵,其实看熟释了就好了,他可比Mov好清楚的多啊,后面所跟的变量所指也比EAX等寄存器清楚的我。
// .method 是说这个区域 是方法 区域指的是 {} 中的内容
// public hidebysig instance 是此方法的属性
// int32 是这个方法的反回值,如果是VB.NET中的 sub 在这里翻译出来返回值为 void
// Level3 是方法名称,与原代码一至
// int32 a 是进入的参数,与原代码一至
// cil managed 是托管方法
// 由于net的一大特性就是MetaData,而它带上了许多的程序信息,所以基本上,il与C#很相以。还是一句老话嘛,凡事有利必有弊。
{
.maxstack 2 // 最大的堆 数量2
// 此值是能过代码中的交换需求计算而来的
.locals init ([0] string s, // 交换变量的类型定义,这里可以看得很清楚。
// 三个变量与一个参数(或返回值)都在这里
[1] unsigned int8[] b,
[2] class [mscorlib]System.Text.ASCIIEncoding asii,
[3] int32 CS$00000003$00000000)
// 下面是代码区
IL_0000: ldstr "215dsgfdart42315s" // 赋值字符器
IL_0005: stloc.0 // 赋值给变量 感觉如 push
IL_0006: newobj instance void [mscorlib]System.Text.ASCIIEncoding::.ctor()
// 建立一个System.Text.ASCIIEncoding对象
IL_000b: stloc.2 // 赋值给变量
IL_000c: ldloc.2 // 取出 System.Text.ASCIIEncoding对象 感觉如 pop
IL_000d: ldloc.0 // 取出 字符串
IL_000e: callvirt instance unsigned int8[] [mscorlib]System.Text.Encoding::GetBytes(string) // 进行转换
IL_0013: stloc.1 // 将结果给 byte[]
IL_0014: ldarg.1 //
IL_0015: ldloc.1 // 取出byte[]
IL_0016: ldlen // 计算长度
IL_0017: conv.i4 //
IL_0018: add // 与 a 相加
IL_0019: starg.s a
IL_001b: ldarg.0 //
IL_001c: ldarg.1
IL_001d: call instance int32 Name1.strong::Level4(int32) //调用 Level4方法
IL_0022: starg.s a
IL_0024: ldarg.1
IL_0025: stloc.3
IL_0026: br.s IL_0028
// 我不知道这里为什么会出现这一句,这一句完全是没必要的
IL_0028: ldloc.3
IL_0029: ret // ret 表示方法结果,如果上面有入栈值,则当成返回变量
}
这样,根据上面
下面是以上四个指令的官方说明:
ldarg.<length> - load argument onto the stack
Format | Assembly Format | Description |
FE 09 <unsigned int16> | ldarg num | Load argument numbered num onto stack. |
0E <unsigned int8> | ldarg.s num | Load argument numbered num onto stack, short form. |
02 | ldarg.0 | Load argument 0 onto stack |
03 | ldarg.1 | Load argument 1 onto stack |
04 | ldarg.2 | Load argument 2 onto stack |
05 | ldarg.3 | Load argument 3 onto stack |
Stack Transition:
… à …, value
Description:
The ldarg num instruction pushes the num’th incoming argument, where arguments are numbered 0 onwards (see Partition I_alink_partitionI) onto the evaluation stack. The ldarg instruction can be used to load a value type or a built-in value onto the stack by copying it from an incoming argument. The type of the value is the same as the type of the argument, as specified by the current method’s signature.
The ldarg.0, ldarg.1, ldarg.2, and ldarg.3 instructions are efficient encodings for loading any of the first 4 arguments. The ldarg.s instruction is an efficient encoding for loading argument numbers 4 through 255.
For procedures that take a variable-length argument list, the ldarg instructions can be used only for the initial fixed arguments, not those in the variable part of the signature. (See the arglistinstruction)
Arguments that hold an integer value smaller than 4 bytes long are expanded to type int32 when they are loaded onto the stack. Floating-point values are expanded to their native size (type F).
Exceptions:
None.
Verifiability:
Correct CIL guarantees that num is a valid argument index. SeeSection 1.5_1.5_OperandTypeTable for more details on how verification determines the type of the value loaded onto the stack.
starg.<length> - store a value in an argument slot
Format | Assembly Format | Description |
FE 0B <unsigned int16> | starg num | Store a value to the argument numbered num |
10 <unsigned int8> | starg.s num | Store a value to the argument numbered num, short form |
Stack Transition:
… value à …,
Description:
The starg num instruction pops a value from the stack and places it in argument slot num (seePartition I_alink_partitionI). The type of the value must match the type of the argument, as specified in the current method’s signature. The starg.s instruction provides an efficient encoding for use with the first 256 arguments.
For procedures that take a variable argument list, the starg instructions can be used only for the initial fixed arguments, not those in the variable part of the signature.
Storing into arguments that hold an integer value smaller than 4 bytes long truncates the value as it moves from the stack to the argument. Floating-point values are rounded from their native size (type F) to the size associated with the argument.
Exceptions:
None.
Verifiability:
Correct CIL requires that num is a valid argument slot.
Verification also checks that the verification type of value matches the type of the argument, as specified in the current method’s signature (verification types are less detailed than CLI types).
ldloc - load local variable onto the stack
Format | Assembly Format | Description |
FE 0C<unsigned int16> | ldloc indx | Load local variable of index indx onto stack. |
11 <unsigned int8> | ldloc.s indx | Load local variable of index indx onto stack, short form. |
06 | ldloc.0 | Load local variable 0 onto stack. |
07 | ldloc.1 | Load local variable 1 onto stack. |
08 | ldloc.2 | Load local variable 2 onto stack. |
09 | ldloc.3 | Load local variable 3 onto stack. |
Stack Transition:
… à …, value
Description:
The ldloc indx instruction pushes the contents of the local variable number indx onto the evaluation stack, where local variables are numbered 0 onwards. Local variables are initialized to 0 before entering the method only if the initialize flag on the method is true (see Partition I_alink_partitionI). The ldloc.0, ldloc.1, ldloc.2, and ldloc.3 instructions provide an efficient encoding for accessing the first four local variables. The ldloc.s instruction provides an efficient encoding for accessing local variables 4 through 255.
The type of the value is the same as the type of the local variable, which is specified in the method header. See Partition I_alink_partitionI.
Local variables that are smaller than 4 bytes long are expanded to type int32 when they are loaded onto the stack. Floating-point values are expanded to their native size (type F).
Exceptions:
VerificationException is thrown if the the “zero initialize” bit for this method has not been set, and the assembly containing this method has not been granted SecurityPermission.SkipVerification (and the CIL does not perform automatic definite-assignment analysis)
Verifiability:
Correct CIL ensures that indx is a valid local index. See Section 1.5_1.5_OperandTypeTable for more details on how verification determines the type of a local variable. For the ldloca indx instruction, indx must lie in the range 0 to 65534 inclusive (specifically, 65535 is not valid)
Rationale: The reason for excluding 65535 is pragmatic: likely implementations will use a 2-byte integer to track both a local’s index, as well as the total number of locals for a given method. If an index of 65535 had been made legal, it would require a wider integer to track the number of locals in such a method.
Also, for verifiable code, this instruction must guarantee that it is not loading an uninitialized value – whether that initialization is done explicitly by having set the “zero initialize” bit for the method, or by previous instructions (where the CLI performs definite-assignment analysis)
stloc - pop value from stack to local variable
Format | Assembly Format | Description |
FE 0E <unsigned int16> | stloc indx | Pop value from stack into local variable indx. |
13 <unsigned int8> | stloc.s indx | Pop value from stack into local variable indx, short form. |
0A | stloc.0 | Pop value from stack into local variable 0. |
0B | stloc.1 | Pop value from stack into local variable 1. |
0C | stloc.2 | Pop value from stack into local variable 2. |
0D | stloc.3 | Pop value from stack into local variable 3. |
Stack Transition:
…, value à …
Description:
The stloc indx instruction pops the top value off the evalution stack and moves it into local variable number indx (see Partition I_alink_partitionI), where local variables are numbered 0 onwards. The type of value must match the type of the local variable as specified in the current method’s locals signature. The stloc.0, stloc.1, stloc.2, and stloc.3 instructions provide an efficient encoding for the first four local variables; the stloc.s instruction provides an efficient encoding for local variables 4 through 255.
Storing into locals that hold an integer value smaller than 4 bytes long truncates the value as it moves from the stack to the local variable. Floating-point values are rounded from their native size (type F) to the size associated with the argument.
Exceptions:
None.
Verifiability:
Correct CIL requires that indx is a valid local index. For the stloc indx instruction, indx must lie in the range 0 to 65534 inclusive (specifically, 65535 is not valid)
Rationale: The reason for excluding 65535 is pragmatic: likely implementations will use a 2-byte integer to track both a local’s index, as well as the total number of locals for a given method. If an index of 65535 had been made legal, it would require a wider integer to track the number of locals in such a method.
Verification also checks that the verification type of value matches the type of the local, as specified in the current method’s locals signature.
所有的官方文档皆在:D:\Program Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Tool Developers Guide\docs。有兴趣的朋友可以阅读一番。
2
3 {
4
5 string s = "215dsgfdart42315s"; // 定义一个字符串
6
7 byte[] b;
8
9 System.Text.ASCIIEncoding asii = new System.Text.ASCIIEncoding();
10
11 b = asii.GetBytes(s); // 将字符串转换为 byte〔〕
12
13 a = a + b.Length; // 然后我要取出 byte的长度,其实就是字符串的长度
14
15 a = Level4(a); // 调用另一个函数
16
17 return a; // 返回 a
18
19 }
20
上面,我们分析了一段IL方法,相信大家对什么是IL有一个认识了,如果想流畅的阅读IL,那么还需要把官方的资料读懂,记牢!
今天我们来讲讲混淆基础。
什么是混淆,故名思意,就是混杂,使界限不分明。这个词相当妙,特别是用在计算机界。
我们知道NET程序集中有一个重要特性叫做MetaData(元数据),它是NET的特性,它记录了相关程序集的一切信息,正因为这样的特性,NET程序集才有了跨平台的可能,才可以在网上传送运行,才不会出现DLL HELL。但同时,由于它记录了所有的信息,使得程序集中的信息可以完全还原至初始状态,使得编译过后的程序集,依然拥有良好的“可读性”。在某些特殊的情况下,这是一件很糟的事情。
对此,我们该怎么做?我们的知识产权无法保障!
正在这样的需求之下,混淆器横空出世。它对MetaData的某些相关信息进行了处理,达到了混淆的作用,在某种意义上讲,它的确能起到一些作用。
最简单的混淆是名称混淆,即将 命名空间名、类名、方法名、字段名等统统换成特殊符号或其它符号,目的就是让你不能与以前的名称建立关联。达到把你弄糊涂的目地。
比方如下代码所示:
{
public bool IsRegistered() // 判断是否已注册的方法
{
return true;
}
}
{
public bool a() // 判断是否已注册的方法
{ return true; }
}
这样,如果你是Cracker,你想破解这个程序,当你看到Register的时候,你就差不多知道这是你要的目标了,但你只看到了 a 你知道是什么吗?所以,在某此情况下,混淆还是有一定作用的。
混淆名称是一件必要的工作。
我们现在深入的谈谈它的优点:
1. 名称混淆,如果使用短名称及不可见字符,将会缩小程序集的大小
2. 名称混淆,使你的程序咋一看上去,更加难以理解。为什么用“咋”这个词?因为名称混淆也只能骗骗门外汉和小孩
那么它有什么缺点呢?
3. 名称混淆的缺点并不多,只有一个,而且非常致命,这就是有时候,当修改了类名之后不能执行的问题。
一般来说,这种情况在DLL身上发生的更多,但在EXE身上也经常发生。
因为DLL的某些Public方法是对外的结口,在程序开发和调试的时候使用的源名称,当混淆以后,天知道把这些方法改成了什么名称,所以调用肯定报错。处理办法:不混淆对外提供的Public方法
EXE和DLL还有一个共同的容易出错的地方就是资源,混淆器也可以混淆资源名称,这样,就存在的同样的问题――“无法找到资源而报错(动态Load资源的时候)”,处理方法:不混淆程序内部调用的东西。
不知道反射可不可以混淆,理论上。。。应该可以的,不过,好象也听过反射混淆后不能运行的案例。
好,总结一下,最简单的混淆就是名称混淆。你也可以自己在程序开发的过程中就这样做,不过。。。很可惜,它起不了多大作用。它的强度远不足以挡住想得到你源代码或侵害你知道产权的人。
那么有没有更好的办法呢?当然有,就是强度更高的流程混淆,感觉和移形换位、乾坤大挪移有点象。为什么这么说?别急,让我喝口水,接着讲。 这样的代码在程序编译后,名称完全被保留,但如果经过名称反混淆以后,它将变成这样:
好,水喝完了,呵呵,可能时间有点久……
现在继续来讲混淆,我们讲到那了?? 哦,流程混淆~~
流程混淆感觉和移形换位、乾坤大挪移有点象……好象已经说过……
为什么这么说呢?因为,流程混淆就是移来移去,达到让你看不懂流程的原理来进行的。
在此,我还要介绍一些其它的知识。由于NET的特性,所以,动态调试NET的全部过程几乎是不可能的,所以,静态分析成为了NET的首选。那么,对付静态分析最好的办法是什么呢?在远古的C时代就已经有这种方法了(混淆其实一点也不新鲜,都是旧技术换个名称而以),那时,这种技术叫作花指令。当然流程混淆和花指令还是有区别的,不过我想,基础的原理也算是差不多了。
什么是花指令?
好,我用汇编构建一段代码如下:
00410072 |. 6A 00 PUSH 0 ; /pModule = NULL
00410074 |. E8 310A0000 CALL <JMP.&KERNEL32.GetModuleHandleA> ; \GetModuleHandleA
00410079 |. A3 B0004200 MOV DWORD PTR DS:[4200B0],EAX
0041007E |. A3 24004200 MOV DWORD PTR DS:[420024],EAX
00410083 |. E8 160A0000 CALL <JMP.&KERNEL32.GetCommandLineA> ; [GetCommandLineA
00410088 90 NOP 《- 注意这里
00410089 90 NOP
0041008A 90 NOP
0041008B 90 /NOP
0041008C 90 NOP
0041008D 90 |NOP
0041008E 90 |NOP
0041008F 90 NOP
00410090 90 |NOP
00410091 90 NOP 《- 还有这里
00410092 |. C705 20004200 >|MOV DWORD PTR DS:[420020],loaddll.00420>; ASCII "Missing DLL name"
0041009C |. E9 EB010000 |JMP loaddll.0041028C
004100A1 |> 3C 22 |CMP AL,22
004100A3 |.^75 E6 \JNZ SHORT loaddll.0041008B
004100A5 |> 8A06 /MOV AL,BYTE PTR DS:[ESI]
004100A7 |. 3C 20 |CMP AL,20
004100A9 |. 75 03 |JNZ SHORT loaddll.004100AE
004100AB |. 46 |INC ESI
004100AC |.^EB F7 \JMP SHORT loaddll.004100A5
00410072 |. 6A 00 PUSH 0 ; /pModule = NULL
00410074 |. E8 310A0000 CALL <JMP.&KERNEL32.GetModuleHandleA> ; \GetModuleHandleA
00410079 |. A3 B0004200 MOV DWORD PTR DS:[4200B0],EAX
0041007E |. A3 24004200 MOV DWORD PTR DS:[420024],EAX
00410083 |. E8 160A0000 CALL <JMP.&KERNEL32.GetCommandLineA> ; [GetCommandLineA
00410088 EB 08 JMP SHORT loaddll.00410092 《- 注意 已经没有00410092
0041008A 2910 SUB DWORD PTR DS:[EAX],EDX
0041008C F8 CLC
0041008D 60 PUSHAD
0041008E 99 CDQ
0041008F F8 CLC
00410090 E8 E8C70520 |CALL 2046C87D
00410095 0042 00 ADD BYTE PTR DS:[EDX],AL
00410098 |? 59 POP ECX
00410099 |? 0142 00 ADD DWORD PTR DS:[EDX],EAX
0041009C |. E9 EB010000 |JMP loaddll.0041028C
004100A1 |> 3C 22 |CMP AL,22
004100A3 |.^75 E6 \JNZ SHORT loaddll.0041008B
004100A5 |> 8A06 /MOV AL,BYTE PTR DS:[ESI]
004100A7 |. 3C 20 |CMP AL,20
004100A9 |. 75 03 |JNZ SHORT loaddll.004100AE
004100AB |. 46 |INC ESI
004100AC |.^EB F7 \JMP SHORT loaddll.004100A5
这就叫做花指令,花指令就是利用跳转或其它的一些指令,并在这此指令中间制造一些无法看懂的代码,使反汇编出来的东西摸不着头脑,并且产生错误的句语(动态跟踪就不会受花指令的影响)从而达到混淆静态反汇编的功能。
那么流程混淆到底是什么呢?
原理基本上是一样,即把方法中的代码分为几段,并把每一段都错开,然后利用“跳转”语句连接原来的流程逻辑,并达到执行正确的目地。原理图如下表所示:
块编号 | 块代码 |
1 | 第一个功能 |
2 | 第二个功能 |
3 | 第三个功能 |
4 | 第四个功能 |
块编号 | 块代码 | 跳转 |
1 | 第一个功能 | Jmp 2 |
4 | 第四个功能 |
|
3 | 第三个功能 | Jmp 4 |
2 | 第二个功能 | Jmp 3 |
基本流程混淆原理即是上表所示,总结就以下这么几个字:破坏原有程序结构,并利用Jmp语句接连原有流程。
基于上面原理所说,所以流程混淆是肯定会耗费资源的。而且,有些特殊的过程,可能在混淆后不能正常使用了,我以前就碰上一个,具体情况记不太清楚,不过第一次运行结果正确,第二次就不正确了,使用DBGCLR跟踪去看,发现第一次执行正常,而第二次则未执行。由于时间非常紧迫,所以未更深入的研究原因所在,当不混淆此方法后,一切正常。
流程混淆是目前各大厂商的混淆利器的最高境界,带有流程混淆的混淆器,基本售价都是上千美元,合人民币近万元。这么高的金额代价之下,它的强度是不是已经能够达到我们的需要了呢?呵呵。这个问题,我们在下一章里再来讨论吧。
附一段 IL 流程混淆前后的代码,明天拿它开刀。
C#源:
private string CreatePassword(char[] passwords,int arraylenghts,int lenghts)
{
int i;
Random RndNumber = new Random();
string return_value="";
for(i=0;i<=lenghts;i++)
{
return_value=return_value+passwords[(int)(RndNumber.NextDouble()*arraylenghts)];
}
return(return_value);
}
IL源:
.method private hidebysig instance string CreatePassword(char[] passwords, int32 arraylenghts, int32 lenghts) cil managed
{
// Code Size: 50 byte(s)
.maxstack 4
.locals (
int32 num1,
[mscorlib]System.Random random1,
string text1)
L_0000: newobj instance void [mscorlib]System.Random::.ctor()
L_0005: stloc.1
L_0006: ldstr ""
L_000b: stloc.2
L_000c: ldc.i4.0
L_000d: stloc.0
L_000e: br.s L_002c
L_0010: ldloc.2
L_0011: ldarg.1
L_0012: ldloc.1
L_0013: callvirt instance float64 [mscorlib]System.Random::NextDouble()
L_0018: ldarg.2
L_0019: conv.r8
L_001a: mul
L_001b: conv.i4
L_001c: ldelem.u2
L_001d: box char
L_0022: call string string::Concat(object, object)
L_0027: stloc.2
L_0028: ldloc.0
L_0029: ldc.i4.1
L_002a: add
L_002b: stloc.0
L_002c: ldloc.0
L_002d: ldarg.3
L_002e: ble.s L_0010
L_0030: ldloc.2
L_0031: ret
}
IL混:
.method private hidebysig instance string CreatePassword(char[] xb97f21c4af3d3653, int32 x37f140bfe992d2c4, int32 x6ad44599b278247e) cil managed
{
// Code Size: 56 byte(s)
.maxstack 4
.locals (
int32 num1,
[mscorlib]System.Random random1,
string text1)
L_0000: newobj instance void [mscorlib]System.Random::.ctor()
L_0005: stloc.1
L_0006: ldstr ""
L_000b: br.s L_0021
L_000d: mul
L_000e: conv.i4
L_000f: ldelem.u2
L_0010: box char
L_0015: call string string::Concat(object, object)
L_001a: stloc.2
L_001b: ldloc.0
L_001c: ldc.i4.1
L_001d: add
L_001e: stloc.0
L_001f: br.s L_0032
L_0021: stloc.2
L_0022: ldc.i4.0
L_0023: stloc.0
L_0024: br.s L_0032
L_0026: ldloc.2
L_0027: ldarg.1
L_0028: ldloc.1
L_0029: callvirt instance float64 [mscorlib]System.Random::NextDouble()
L_002e: ldarg.2
L_002f: conv.r8
L_0030: br.s L_000d
L_0032: ldloc.0
L_0033: ldarg.3
L_0034: ble.s L_0026
L_0036: ldloc.2
L_0037: ret
}
明天来讲讲反流程混淆,其实人人都可以做到同时,有说得不对之处,还请高手赐教 修改了一下:
这几天,工作特别忙,没空抽出时间来写文章,粗糙之作,还请见谅。
1. 名称混淆 - 反混淆
名称混淆返混淆,基本上是不太可能的事,因为以前的名称已经换掉了,也没有
第二个名称备份表,
所以根本无法还换。
不过,可以把不可见字符转换为可见字符,长字符串换成短字符串。
有两种方法可以做处理:
1. 在MetaData中有一个区域叫做 _STRING 它存放了所有名称字符串,只要修
改这里的内容,即可
,此方法需要对元数据结构特别熟悉
2. 如果你对元数据不了解,没关系,你可以用ILDasm把混淆后的程序集反编译
,然后一个一个的对
应改过来,再用ilAsm编译,一样可以达到反混淆的作用
其实,对名于名称来说,真的没有多大用处,不用反混淆也行,免得浪费自己的时间。
2. 流程混淆 - 反混淆
流程混淆,在上面已经给出例子。它才是有用的一种混淆方式。它改变流程的存放序
顺,从而达到静态反编译的功能。(名称混淆还是可以反编译)
不过,不管怎样,他没有办法去阻止读取IL,这就是流程混淆的天生不足。我们来看
看如何对流程反混淆吧。
还是以上面的例子进行操作。
首先特别说明一下: br.s 行号 br 行号 都是强行跳转指令,而流程混淆主要是得用
这样的语句进行逻辑连接的。
所以,我们就需要对照着被混淆过的代码,跟着一句一句的逻辑关系,把语句拉出来
重新组合。
组合出来后,代码如下:
L_0000: newobj instance void [mscorlib]System.Random
::.ctor()
L_0005: stloc.1
L_0006: ldstr ""
L_0021: stloc.2
L_0022: ldc.i4.0
L_0023: stloc.0
L_0024: br.s L_0032
L_0026: ldloc.2
L_0027: ldarg.1
L_0028: ldloc.1
L_0029: callvirt instance float64 [mscorlib]System.Random
::NextDouble()
L_002e: ldarg.2
L_002f: conv.r8
L_000d: mul
L_000e: conv.i4
L_000f: ldelem.u2
L_0010: box char
L_001a: stloc.2
L_001b: ldloc.0
L_001c: ldc.i4.1
L_001d: add
L_001e: stloc.0
L_0032: ldloc.0
L_0033: ldarg.3
L_0034: ble.s L_0026
L_0036: ldloc.2
L_0037: ret
其实,反流程混淆也相当的容易,只要按照执行流程加入特定的条件即可以得到代码
的序顺。
为此,我特别写了一个反流程混淆的工具(Deflow)。将上面的代码反混淆后,得到
如下代码:
L_0000: newobj instance void [mscorlib]System.Random::.ctor()
L_0001: stloc.1
L_0002: ldstr ""
L_0003: stloc.2
L_0004: ldc.i4.0
L_0005: stloc.0
L_0006: br.s L_0017
L_0007: ldloc.2
L_0008: ldarg.1
L_0009: ldloc.1
L_000A: callvirt instance float64 [mscorlib]System.Random::NextDouble()
L_000B: ldarg.2
L_000C: conv.r8
L_000D: mul
L_000E: conv.i4
L_000F: ldelem.u2
L_0010: box char
L_0011: call string string::Concat(object, object)
L_0012: stloc.2
L_0013: ldloc.0
L_0014: ldc.i4.1
L_0015: add
L_0016: stloc.0
L_0017: ldloc.0
L_0018: ldarg.3
L_0019: ble.s L_0007
L_001A: ldloc.2
L_001B: ret
反流程混淆,并不难,说句实话,它只是一个特征的积累,你不断的分析被混淆后
的特征,然后进行分析,写出反混淆的算法,即可开发出反混淆工具。上面的程序并不长
,你可以手工的进行反混淆,但如果很长,那么反混淆就是一件痛苦的事了,但我只用
Deflow,请请点一下按钮,就可以得到我感兴趣的代码。而且,只要是编程的人,不要
你全面撑握NET什么的知识,只要你对字符串操作特别熟悉,再加上一些些聪明和经验,
反流程混淆不是一件难事。
下面,我给大家介绍我反混淆的步骤:
1. 拿到混淆过后的程序集(当然,是流程混淆过的)
2. 使用我修改过的Ildasm进行反编译
3. 得到一个IL文件(明文格式的文本文件)
4. 打开文件,找到自己感兴趣的代码,把那一段方法取出
5. 使用Deflow反混淆
6. 把反混淆后的代码Copy回去
7. 使用ilasm 重新编译il文件
8. 使用Reflector查看,ok,已经可以显示C#或VB的代码了。
由于,反编译出来的il是明文,所以,只要你对字符串操作比较熟,你就可以做一个
非常完善的针对工程的反混淆工具。反混淆原理就是如此,不管反混淆强度如何改
变,它也无法脱理这个原理,所以即使是流程混淆,也无法保证程序代码的安全性。
这里特别说明一下:Deflow纯属研究性质,所以功能非常有限,我也不会发布和公
开,在研究的过程中发现有些混淆器相当狡猾,它把程序正的跳转和混淆跳转再次
混淆,这种情况,我们有时候必须要手动干涉,不过,这种情况并不会带来多少工
作量。至少我现在还没有手工改过。
总之,反流程混淆的工具,是每个程序员都可以自己行开发的,因为起点太低,任
何人了解原理后都可以对其进行研究,并开发出反混淆的工具。所以,流程混淆并
不理想!
更重要的是,即使流程混淆了,也可以进行反编译,从而,关键代码还是会被修改
并再次发布,危险性依然存在。我就不明白为什么MS要提供ilDasm和ilAsm。这不
是制造木马的好工具吗?把别个的程序反编译,在里面某个铵钮中加入自己的代码。
再编译,打包,当正版发布,神不知,鬼不觉的让你中马,结果机器的主人还要找
软件作者去算账……呵呵,怕怕啊。
下面,我们将介绍一下最新的MaxtoCode的原理。有兴趣的朋友不要错过。
新一代NET代码保护工具 MaxtoCode 的原理简介
MaxtoCode作为新一代保护工具,他有绝对优于目前所有NET代码保护工具的强度,是目前最保护强度最高的解决方案。但它也有缺点,最大的缺点即是:被加密后的程序集只能在WINDOWS平台下运行,而不能跨平台。但,MS的战略影响,几乎不可能出现官方非WINDOWS平台的NET运行环境,所以,此缺点先忽略不计,也许随着版本的升级,MaxtoCode可能会考虑跨平台的应用。
MaxtoCode为什么说是目前最强壮的NET代码保护方案呢?
如果大家是老程序员,应该知道Win32中的壳保护方案。这是一个非常好的方案,不过可惜,壳的最终较量变成了Anti debug的较量。因为壳有一个致命的弱点,即在会在内存中完全解开被加壳的程序代码,并将执行指针指向被加壳的程序代码第一行。所以,只要找到被加壳的程序代码第一行,即可以得到原来的程序。这种方式被业界叫做脱壳。
MaxtoCode 的原身不是NET语言编写,它借用了Win32的壳概念,并更优于Win32的壳概念(这也是因为NET的特点才能达到这样的程度)。MaxtoCode借助门槛更高的ASM语言,对NET代码进行加密,从而得到了更强壮的NET代码保护方案。
MaxtoCode可以理解成NET的壳,但是它是一个不容易被脱掉的壳,如果说Win32的壳是件外套,那么MaxtoCode就是皮肤。壳的特点是在内存中释放所有的代码,而MaxtoCode不会这样做。它比Win32的壳更加安全,它的安全性处决于MaxtoCode的加密算法。这正是MaxtoCode要达到的目地。因为理论上不可能有无法破解的程序,我们要的是无限提高破解成本的保护方案,来迫使破解者放弃对程序的破解。如果MaxtoCode能把破解强度建立在加密算法上,那么,MaxtoCode将能大幅度的提高保护强度。要知道,一,二种算法可能很好破解,可当有一,二十种算法、而且其中还有大量的高强度算法的变种算法,那么,想要完全解出这些算法是一件头痛的事情。最重要的是MaxtoCode还在不停的增加算法,来增加MaxtoCode的安全性。
讲了那么多废话,下面,我们来讲述一下MaxtoCode的原理:
首先先说一下CLI的解释原理:
CLI接到程序集需要解释的请求后,动态的从内存中取出一段代码(IL)然后交由CLI解释,CLI解释成机器语言,然后提交给CPU执行,在此中间,还产生大量的静态缓存。特别需要注意的是,CLI是按Method进行解释的。
正因为这样的原理,MaxtoCode则有空间将ASM引入到Method中去。
MaxtoCode的大致工作原理如下:
当CLI提取Method的IL代码时,MaxtoCode提供的解释环境将Method的IL代码解密回去。
当CLI提取完毕后,MaxtoCode将Method的IL代码再进行破坏,达到加密的目地
原理表:
CLI > MaxtoCode > IL > CLI > MaxtoCode > 加密
从而保证不会象原始的Win32壳一样留下代码在内存中。也保证了将NET的保护安全建立在众多的加密算法之上。
同时,由于在未被MaxtoCode解密的情况下是乱码,根本无法被静态反编译,所以不管是高级的反编译,还是低级的反编译,都无法得到一个可读或可看的程序集。
动态加、解密成本相对来说提高,所以MaxtoCode在处理结构上做了更多的优化,已经把结构处理得相当的优化,所以,在某些特写的程序上几乎感觉不到MaxtoCode的存在,而且MaxtoCode强制了NET的缓存机制,“一般加密方法”还可以提高运行速度1%~5%。重要的方法使用“高级加密方法”可以得到非常稳定的保护(这里要负责任的说一句,高级加密方法会降低运行效率,根据不同的程序产生不同的影响,最好的效果是完全感觉不到。但我们希望不要全部使用高级加密方法进行加密,而是酌情考虑使用和先测试再使用)。
―――――――――――――――我是无敌分隔线―――――――――――――――――
其它保护方法
还有一些方法,是无法使用工具去实现的,只有手工去做。
如:类折分,方法折分
一个类,写完后,手工折成上百个类
一个大方法,折成上百个子方法
别觉得这不可能哦,我可亲眼见过这样的程序,不过,从安全性来说……没多大意义,因为Reflector的功能真的太强大,只要你是能够看得见的结构,都逃不了被Reflector分析得一个透彻。
还有一著名的保护方法:Reflector的保护方法,把自己的核心程序做成一个资源包,然后加密,当需要的时候动态加载,只要你在载入的时候够复杂,还是有一定的保护作用的。其实,这种方法的破解有更好的方法,可以只需要几分钟就可以解破。但今天它不是我们需要讲解的范围,故不详细解说了。免得某些用此方法的同志围抠我。呵呵。
好,至此,从浅到深,谈谈NET混淆原理已经讲完了,由于时间很忙,无法给出更多的例子和实战经验,所以希望大家能够举一反三,自己多演练,多研究。如果那天有那位大侠要发布反流程混淆的工具,记得给小弟我留一份,非常感谢。