`
pcajax
  • 浏览: 2173799 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

.net 代码混淆原理性实践

阅读更多

现在我们已经很清楚,托管PE文件可以轻而易举的被反编译,如果您想源代码不被使用者通过反编译的方式获得,该使用哪种保护手段呢?

借鉴传统Windows应用程序防止被反汇编的做法,我们也可以采用代码混淆和对应用程序集加壳的方法。关于程序集加壳的内容我会在下一篇文章中讨论。

代码混淆,简单的说就是使用名称替换、字符串加密等手段使得我们最初的代码面目全非,从而使破解者即使能能成功获得IL代码也很难得到想要的源代码。代码混淆常用的方式有名称混淆、流程混淆和语法混淆。

9.3.1 名称混淆

在讲解名称混淆的原理之前,我们先建一个用于测试的控制台程序,如代码清单9-8所示。

代码清单9-8 名称混淆测试代码

class Program
    {
static void Main(string[] args)
        {
string userID="asldjf3333djf";
string pwd = GetPassword(userID);
            ChangeInfo(userID, pwd);
        }
public static string GetPassword(string userID)
        {
return "123456";
        }
public static void ChangeInfo(string userID,string pwd)
        {
        }
    }

代码清单9-8中的代码很简单,包含了三个函数Main函数、GetPassword函数和ChangeInfo函数。从函数的名称和参数中,我们很容易想到其含义。接下来,编译项目,然后使用Reflector打开生成的EXE文件,查看Program类的IL代码。如图9-14所示。

clip_image002

图9-14 Program类的IL代码

下面我们使用PE文件查看工具CFF载入可执行文件。定位到#Strings流,如图9-15所示。当前程序集的类型、引用类型、成员等的定义都在该字符串中。

clip_image004

图 9-15 Program.exe 的#Strings流

下面我们使用CFF来修改#Strings流的内容。查找到字符串“ChangeInfo”和字符串“GetPassword”,随意替换,然后运行程序验证是否有问题。

那么这样的修改有什么作用呢,我们保存修改后,使用Reflector重新打开exe文件,如图9-16所示。

clip_image006

图9-16 修改#Strings流后的IL代码

从9-16中,我们可以看到,先前的ChangeInfo和GetPassword方法名已经被替换成不可识别的乱码。

在完整的实践上面的演示之后,我想告诉你的是,你已经明白了名称混淆的原理。现在简单的总结一下,所谓.NET名称混淆就是修改#Strings流内特定字符串,使其不能 被轻易辨认。上面的例子只修改了两个方法的名称,当然我们可以修改包括在变量在内的所有名称来迷惑“对手”。如果您要问我,如果一个大型的软件项目,这样手动修改可行吗?当然不可行,但是原理知道了,编写这样一个工具并非难事。当然现在已经出现很多成熟的名称混淆工具,这里就不给您一一介绍了。

明白了原理之后,我们再看字符串替换的方式到底有哪些。

第一种替换方法为无意义替换。我们知道在实际的开发过程中,我们都必须遵守一定的命名规范来为类型、属性、字段命名。但是当我们对编译成功的代码进行名称混淆的时候就是要将这些规范的、规律的名称变得毫无意义,毫无规律可循。

第二种替换方法称为不可打印字符替换。在UNICODE字符集中,一些特殊字符,目前无法得到正确的显示,比如从0x01到0x20之间的字符。如果把名称替换成这些字符,显示出来的就是奇怪的乱码。

第三种替换方法为空字符替换。空字符替换就是把名称替换为空串。但是这种方法并不适合实际的应用,因为如果名称都为空,那么势必要产生二义性。

在实际环境中,我们常常要引用其他程序集。如果被引用的程序集的方法名称没有被混淆的话,那么在本程序集中混淆引用的方法名是无效的,会引发调用异常。这是名称混淆最大的局限性。

9.3.2 流程混淆

流程混淆是指打乱方法的流程,让反编译软件无法将代码正确的反编译为高级语言。流程混淆即可保护代码又可增加破解者分析代码的难度。

目前流程混淆基本上都是基于跳转实现的,我们在程序中使用的跳转有如下三种方式:

1) goto跳转;

2) if-else跳转

3) switch跳转。

.NET的流程混淆和传统windows应用程序的流程混淆本质上是有区别的。传统的流程混淆的目的是防止反汇编,而.NET流程混淆的目的是防止反编译。传统的流程混淆是基于汇编指令进行的,操作层次较低,可更改堆栈调用,可操作方式较多;.NET流程混淆是基于IL指令进行的,操作层次较高,不可触及堆栈调用,可操作方式较少。

下面我们针对代码清单9-9和代码清单9-10的示例程序,来实践流程混淆的不同方式。

代码清单9-9 测试流程混淆代码

class Program
    {
static void Main(string[] args)
        {
string h = "hello";
if (h == "hell0")
            {
                OutString();
            }
else
            {
Console.WriteLine("There is no hello!");
            }
        }
public static void OutString()
        {
Console.WriteLine("hello");
        }
    }

现在编译项目,然后使用ILDasm导出改程序的IL代码(只截取Class IL代码部分),如代码清单9-10所示。

代码清单9-10 代码清单9-9的IL代码(Class IL代码部分)

// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit FlowObufscation.Program

extends [mscorlib]System.Object

{

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

// 代码大小 49 (0x31)

.maxstack 2

.locals init ([0] string h,

[1] bool CS$4$0000)

IL_0000: nop

IL_0001: ldstr "hello"

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: ldstr "hell0"

IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)

IL_0012: ldc.i4.0

IL_0013: ceq

IL_0015: stloc.1

IL_0016: ldloc.1

IL_0017: brtrue.s IL_0023

IL_0019: nop

IL_001a: call void FlowObufscation.Program::OutString()

IL_001f: nop

IL_0020: nop

IL_0021: br.s IL_0030

IL_0023: nop

IL_0024: ldstr "There is no hello!"

IL_0029: call void [mscorlib]System.Console::WriteLine(string)

IL_002e: nop

IL_002f: nop

IL_0030: ret

} // end of method Program::Main

.method public hidebysig static void OutString() cil managed

{

// 代码大小 13 (0xd)

.maxstack 8

IL_0000: nop

IL_0001: ldstr "hello"

IL_0006: call void [mscorlib]System.Console::WriteLine(string)

IL_000b: nop

IL_000c: ret

} // end of method Program::OutString

.method public hidebysig specialname rtspecialname

instance void .ctor() cil managed

{

// 代码大小 7 (0x7)

.maxstack 8

IL_0000: ldarg.0

IL_0001: call instance void [mscorlib]System.Object::.ctor()

IL_0006: ret

} // end of method Program::.ctor

} // end of class FlowObufscation.Program

// =============================================================

// *********** 反汇编完成 ***********************

// 警告: 创建了 Win32 资源文件 D:\FlowObufscation.res

代码清单9-10为代码清单9-9中整个Program类对应的IL代码。下面结合这两段代码进行流程混淆的实践。

q 代码块易位

Main方法内,代码从IL_0000一直到IL_0030,下面我们将这段代码分成三段,如代码清单9-11所示。

代码清单9-11 把Main方法分成三段

.method private hidebysig static void Main(string[] args) cil managed

{

//第一段开始 

.entrypoint

// 代码大小 49 (0x31)

.maxstack 2

.locals init ([0] string h,

[1] bool CS$4$0000)

IL_0000: nop

IL_0001: ldstr "hello"

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: ldstr "hell0"

IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)

//第一段结束 

//第二段开始 

IL_0012: ldc.i4.0

IL_0013: ceq

IL_0015: stloc.1

IL_0016: ldloc.1

IL_0017: brtrue.s IL_0023

IL_0019: nop

IL_001a: call void FlowObufscation.Program::OutString()

IL_001f: nop

IL_0020: nop

IL_0021: br.s IL_0030

IL_0023: nop

IL_0024: ldstr "There is no hello!"

//第二段结束

//第三段开始

IL_0029: call void [mscorlib]System.Console::WriteLine(string)

IL_002e: nop

IL_002f: nop

IL_0030: ret

//第三段结束

} // end of method Program::Main

如代码清单9-11,我将Main方法的IL代码分成了三段,分段并没有什么依据,完全是随意的。下面我们将这三段代码重新组合,按照第一段、第三段、第二段的顺序排列,然后在第一段的结尾加上跳转语句“br IL_0012”,在第二段代码的后面加上跳转语句“br IL_0029”。修改之后的代码如代码清单9-12所示。

代码清单9-12重新排列的Main方法

.method private hidebysig static void Main(string[] args) cil managed

{

//第一段开始

.entrypoint

// 代码大小 49 (0x31)

.maxstack 2

.locals init ([0] string h,

[1] bool CS$4$0000)

IL_0000: nop

IL_0001: ldstr "hello"

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: ldstr "hell0"

IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)

br IL_0012

//第一段结束

//第三段开始

IL_0029: call void [mscorlib]System.Console::WriteLine(string)

IL_002e: nop

IL_002f: nop

IL_0030: ret

//第三段结束

//第二段开始

IL_0012: ldc.i4.0

IL_0013: ceq

IL_0015: stloc.1

IL_0016: ldloc.1

IL_0017: brtrue.s IL_0023

IL_0019: nop

IL_001a: call void FlowObufscation.Program::OutString()

IL_001f: nop

IL_0020: nop

IL_0021: br.s IL_0030

IL_0023: nop

IL_0024: ldstr "There is no hello!"

br IL_0029

//第二段结束

} // end of method Program::Main

现在我们使用ILAsm重新编译修改后的il代码,验证确认可以正常运行。如图9-17所示。

clip_image008

图9-17 重新编译修改过的IL

如图9-17,我重新编译修改过的IL文件,并在命令行下运行生成的exe文件,程序正常运行。但是这样的跳转对保护程序有什么意义吗?我们带着这个疑问使用Reflector打开生成的exe文件。当我们尝试将Main方法转成C#代码时,Reflector抛出了异常。如图9-18所示。

clip_image010

图9-18 Reflector打开代码段易位的程序集报错

图9-18中,我使用Reflector报的竟然是为将对象引用设置到对象实例的错误,看了是引发了Reflector的内部错误。

结果是这样的,但是原因呢,为什么IL运行运行的代码,Reflector却在反编译的时候出错呢?实际上这和我们设置的跳转点有关系。CLI规定程序走入任何分支时,要保持堆栈为空,如果我们在堆栈不为空的时候跳转,反编译器依照CLI进行代码反编译时就会出错。如果我们的跳转点设置在堆栈为空的地方,那么反编译是不会出错的。

q 连续跳转

基于上面提到的代码易位,很有效的达到了阻止反编译的目的,但是,在自动化程序中如何有效的去设置其跳转点呢?针对大块代码的跳转点设置,目前仍没有好的解决方案。于是有人提出了针对每一条IL指令(或者很少的几条)做跳转。这种方法被称作连续跳转。

很显然,连续跳转的保护强度要高于代码块易位的方法。想要手工修复连续跳转,对于大型软件来说几乎是不可能的。

连续跳转还有一种“变体”。我们事先设置好一连串的跳转指令,这样我们可以在需要跳转的地方每次自动跳转固定次数然后再转到目标点。

q 逻辑跳转

上面的各种跳转方法都是直接跳转,为了增加跳转的复杂度,我们可以对各个跳转增加逻辑判断。当然我们的逻辑判断只能是恒真或者恒假,虽然如此但是其保护强度却大大加强了。

现在我们对代码清单9-12作少量的修改,给两处跳转指令加上条件判断。修改后的代码如代码清单9-13所示。

代码清单9-13 逻辑跳转示例

.method private hidebysig static void Main(string[] args) cil managed

{

//第一段开始

.entrypoint

// 代码大小 49 (0x31)

.maxstack 2

.locals init ([0] string h,

[1] bool CS$4$0000)

IL_0000: nop

IL_0001: ldstr "hello"

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: ldstr "hell0"

IL_000d: call bool [mscorlib]System.String::op_Equality(string, string)

Ldc.i4.1

Ldc.i4.0

Bgt IL_0012

//第一段结束

//第三段开始

IL_0029: call void [mscorlib]System.Console::WriteLine(string)

IL_002e: nop

IL_002f: nop

IL_0030: ret

//第三段结束

//第二段开始

IL_0012: ldc.i4.0

IL_0013: ceq

IL_0015: stloc.1

IL_0016: ldloc.1

IL_0017: brtrue.s IL_0023

IL_0019: nop

IL_001a: call void FlowObufscation.Program::OutString()

IL_001f: nop

IL_0020: nop

IL_0021: br.s IL_0030

IL_0023: nop

IL_0024: ldstr "There is no hello!"

Ldc.i4.1

Ldc.i4.0

sub

brtrue IL_0029

ret

//第二段结束

} // end of method Program::Main

在代码清单9-13中,我们对两处跳转指令做了修改,第一处修改为:

Ldc.i4.1

Ldc.i4.0

Bgt IL_0012

相当于C#中的c#中的:

if(1>0)

{goto IL_0012;}

第二处跳转被修改为:

Ldc.i4.1

Ldc.i4.0

sub

brtrue IL_0029

ret

相当于C#代码的:

if(1-0==true)

{

Goto IL_0029;

}

将直接跳转改为逻辑跳转,为反混淆增加了代码识别的难度,因为有时候很难判断代码if条件是源代码就有的还是后来为了混淆才添加上的。当然我们也可以仿造直接跳转的方式,增加多层条件判断来增强保护强度。

增强逻辑跳转的迷惑性的另一个方法是添加临时变量,并在条件判断中使用临时变量。

但是在IL中是允许的,那么如果我们在IL中加入这种判断,反编译时一定会报错的。

q Switch跳转

Switch跳转的基本基本原理和上面的提到的方法大同小异。基本方法为把程序中那个的每条指令都放在switch的判断中。这样就需要建立多个局部变量来进行判断。这种方法无疑会增加太多的垃圾代码。

Switch跳转的例子这里就不做演示了。如果您感兴趣可自行实践。

q 利用语言差异

我们还有一个更强的保护手段,就是利用IL语言和高级语言之间的差异性。我们知道,并不是IL语言所有的特性都会反映在高级语言中。高级语言只是IL的子集而已。

比如在c#中这样的代码是不允许的:

if(12.32)

{

//do something

}

关于代码混淆的技术不只这些,由于篇幅所限我们暂且讨论至此。在实际应用中我们也不可能手动的去做代码混淆,有很多成熟的工具可以供选择。

分享到:
评论

相关推荐

    .net 代码反编译工具

    本文将详细介绍.NET代码反编译工具及其相关知识。 标题中的".NET 代码反编译工具"是一种专门用于查看.NET程序IL(Intermediate Language)代码或试图还原出接近原生源代码的软件。这种工具可以帮助开发者学习他人...

    .NET反混淆器:.NET反混淆器和解压缩器的列表(开放源代码)

    在.NET开发过程中,有时开发者或安全研究人员需要对已混淆的.NET代码进行逆向工程,以理解其内部工作原理、查找潜在问题或保护自己的代码不被逆向分析。混淆是一种常见的代码保护策略,它使得代码变得难以阅读和理解...

    .net简易名程混淆工具源码

    在这个场景中,我们关注的是一个名为".NET简易名程混淆工具源码"的项目,它涉及到源代码混淆这一关键的安全实践。 混淆工具是编程领域中的一个重要概念,尤其是在.NET环境中,它的主要目的是保护.NET程序的源代码不...

    Eziriz .Net Reactor v4.2.8.4加密程序

    这样使得你的程序集在运行态和设计态的时候才被还原(也就是说任何工具都无法访问时),任何工具都不能反编译 .NET Reacto代码混淆r保护的程序集。.NET Reactor在黑客和你的程序集之间构建了一道原生的“代码墙”,...

    防止反编译及使.net程序脱离.net环境的工具

    总结来说,防止.NET程序反编译和使其脱离.NET环境运行,需要结合多种技术手段,包括但不限于代码混淆、加密、IL注入、自定义运行时环境等。同时,法律保护和良好的开源策略也是确保代码安全的重要环节。

    .net 反编译器——Reflector

    4. **插件支持**:Reflector支持第三方插件,这些插件可以扩展其功能,例如代码分析、性能测试、代码混淆等。例如,Lutz Reflective Logger和Disassembler Pro都是非常流行的Reflector插件。 5. **资源查看**:...

    .NET反编译工具

    这类工具对于理解代码逻辑、调试、学习第三方库的工作原理以及代码审计等场景非常有用。 Reflector.exe是一款知名的.NET反编译器,由Lutz Roeder开发,后来被Red Gate Software收购。它能够打开和分析.NET程序集,...

    ASP.NET源码——ASP.NET Web BackDoor.zip

    这个"ASP.NET Web BackDoor 1.0_aspxbackdoor"的源码分析可以帮助我们理解后门的工作原理,进一步提高对网络安全的警惕性。同时,学习如何防止和检测这类威胁对于维护Web应用程序的安全至关重要。在研究这个源码时,...

    最新几款.net反编译程序

    5. **DeepCode .NET Decompiler**: DeepCode是一款高级的反编译工具,它不仅可以反编译.NET代码,还能处理加密和混淆的代码。其强大的分析引擎可以帮助开发者更深入地理解复杂的程序逻辑。 6. **Opinionated .NET ...

    .net 反编译.net 反编译.net 反编译

    5. **反混淆**: 当代码被混淆处理过,反编译后可能会更加难以解读。在这种情况下,可能需要额外的工具或技术来还原代码的可读性。 了解.NET反编译对于开发者来说,可以提升其在调试、学习和代码维护等方面的能力。...

    aspx后门.net

    UPX是一种知名的可执行文件压缩工具,有时黑客会使用它来混淆恶意代码,使得病毒、木马或后门更难以被检测到。在这里,"upx8"可能是指经过UPX压缩的恶意.NET组件。 标签 ".net"、"aspx" 和 "后门"、"webshell" 提供...

    .NET软件授权源码-十分完整

    在实践中,可以参考开源的授权库,如License.NET,它们提供了现成的解决方案和示例代码,有助于快速搭建授权系统。 然而,需要注意的是,软件授权不仅是一个技术问题,还涉及到法律和商业策略。开发者应遵循相关...

    .net程序反编译软件

    这类软件能够将已编译的.NET代码转换回可读的C#、VB.NET或其他.NET语言的源代码,以便于学习、调试或分析其他人的软件。虽然不能保证完全还原所有原始源代码,但通常可以获取大部分功能和逻辑。 标题中提到的".NET...

    .net 反编译工具

    9. **安全与隐私**:由于反编译能揭示代码细节,因此对于处理敏感信息的软件,开发者需要采取反逆向工程措施,如代码混淆、加密或使用不可逆的编译技术。 10. **开发与调试**:反编译工具在开发过程中也发挥重要...

    Reflector(.Net映射)

    - **代码审计**:检查代码安全性和性能瓶颈,确保代码符合最佳实践和安全标准。 - **教学培训**:在教学环境中,Reflector可以直观展示.NET代码的工作机制,加深学员对.NET平台的理解。 3. **主要功能**: - **...

    NETReactorSlayer-3.1.0.0-rc1.zip

    总之,下载并研究.NETReactorSlayer的源码不仅有助于你理解.NET代码保护的原理,还能提升你在.NET开发领域的专业技能,特别是涉及到逆向工程和代码安全的方面。通过深入学习和实践,你将能够更好地应对复杂的代码...

    C#混淆器源代码 用C#所写,包括设计文档

    通过分析这些代码,我们可以学习到如何在.NET环境中实现代码混淆,以及如何处理元数据和IL指令。 3. `bin`:这个目录通常存放编译后的可执行文件和其他依赖项。在这个案例中,可能包含了混淆器的二进制版本,我们...

    de4dot源码

    总结,de4dot源码是一个宝贵的资源,它揭示了.NET去混淆的复杂性和艺术性。通过深入学习和理解,开发者不仅可以掌握反混淆技术,还能提升在.NET开发领域的综合技能。无论是为了工作需求还是个人兴趣,de4dot都值得...

    Obfuscatorks:一个简单的程序,以了解.NET混淆器如何与任何方法一起使用

    本文将深入探讨.NET混淆器的工作原理、重要性以及如何在实际项目中应用。 标题“Obfuscatorks:一个简单的程序,以了解.NET混淆器如何与任何方法一起使用”暗示我们将探讨一个特定的混淆器实例——Obfuscatorks,它...

    .NET反编译工具 Reflector.exe

    同时,开发者也可以通过混淆技术来保护自己的.NET代码,减少被反编译的风险。 7. **学习与应用**:对于初学者,了解反编译工具可以帮助深入理解.NET框架,提升编程技能。对于高级开发者,Reflector可以帮助调试难题...

Global site tag (gtag.js) - Google Analytics