.NET体系中的源程序安全问题, Delegates


 Delegates
1  .NET中的委派(Delegates)
      回调函数的确是至今为止最有用的编程机制之一。C运行时的qsort函数利用回调函数对数组元素进行排序。在Windows中,回调函数更是窗口过程,钩子过程,异步过程调用,以及目前Microsoft .NET框架所必需的,在整个回调过程中自始至终地使用回调方法。人们可以注册回调方法以获得加载/卸载通知,未处理异常通知,数据库/窗口状态修改通知,文件系统修改通知,菜单项选择,完成的异步操作通知,过滤一组条目等等。
      在C/C++中,一个函数的地址就是内存地址。这个地址不会附带任何其它赋加信息,如函数的参数个数,参数类型,函数的返回值类型以及这个函数的调用规范。简言之,C/C++回调函数不是类型安全的。
      在.NET框架中,回调函数所受到的重用与它在Windows非受控编程中一样。不同的是在.NET框架中提供了一种叫委派(delegates)的类型安全机制。
委派示例:
      //声明一个处理字符串的委派
      public delegate void Feedback( string str);
      //定义个类MyClass
      public class MyClass
      {
           //只要其执行该方法的同时,feedback非空,则将调用feedback链表中所有的方法
           //这就如同Windows事件(消息通知)
          public void ProcessEvent(Feedback feedback)
          {
            System.Console.WriteLine(“首先:方法原本处理的内容!”);
            if (feedback !=null){
            //如果feedback非空,则通知feedback…
            feedback(“而后:”+this.ToString()+”调用了函数ProcessEvent()”); }
          }
          //委派处理
          public void FeedbackToConsole(string str)
          {System.Console.WriteLine(str);}
      }  
  
      //客户端代码
      MyClass myClass = new MyClass();
      myClass.ProcessEvent(new Feedback(myClass.FeedbackToConsole));
  输出:
      首先:方法原本处理的内容!
      而后:MyClass调用了函数ProcessEvent()
      当编译器到遇到Delegate类的声明时,它会产生一个完整的类定义:如上的Feedback我们有:
      public class Feedback : System.MulticastDelegate {
         public Feedback(Object target, Int32 methodPtr); //构造器
         // 方法Invoke与源代码描述的原型相同
         public void virtual Invoke( Object value, Int32 item, Int32 numItems);
         // 方法BeginInvoke允许被异步回调
         public virtual IAsyncResult BeginInvoke(Object value,
                                              Int32 item, Int32 numItems,
         AsyncCallback callback, Object object);
         public virtual void EndInvoke(IAsyncResult result);
      }
      MulticastDelegate类从Delegate类中派生并赋予了创建MulticastDelegate对象链表(或者说链)的能力。
      所有的委派都派生于MulticastDelegate,它们继承了MulticastDelegate的域,属性和方法(delegate不同于Delegate,后者是个类)。在所有这些成员中,你要特别注意三个私有域:
      _target System.Object: 指回调函数被调用时应该操作的对象。用于实例方法回调;
      _methodPtr System.Int32: 内部整型,CLR用它来标示被回调的方法 ;
      _prev System.MulticastDelegate: 指另一个委派对象,通常为null。
      所有的委派都有带两个参数的构造器:一个参数是对象引用,一个是指代回调方法的整型。上面实例中有:
      public Feedback(Object target, Int32 methodPtr);
      Delegate类Equals 方法:检查它们的_target和_methodPtr指针域是否引用相同的对象和方法。
      // 构造两个委派对象,它们引用相同的目标/方法
      Feedback fb1 = new Feedback(FeedbackToConsole1);
      Feedback fb2 = new Feedback(FeedbackToConsole2);
      // 虽然 fb1 和 fb2 引用两个内部不同的对象,但都引用相同的回调目标/方法
      Console.WriteLine(fb1.Equals(fb2)); // 显示Displays "True"
      此外,Delegate 和 MulticastDelegate都提供相等(==)和不等(!=)操作的重载。既有:
      Console.WriteLine(fb1==fb2)); //等价fb1.Equals(fb2)

4.2  委派链(Delegate Chains)
      在处理委派链的时候,理解如何比较两个委派的相等性是很重要的,下面我们就来讨论委派链。
      MulticastDelegate对象都具备一个私有的_prev域。该域存储对另一个MulticastDelegate对象的引用。也就是说,每一个MulticastDelegate对象类型(或其派生类)都具备对另一个MulticastDelegate派生对象的引用。这个域允许将委派对象加到某个链表中。
Delegate类定义了三个静态方法,可以用它们来处理委派对象的链表链:
      class System.Delegate {
         // 这个函数联合由head & tail表示的链,head 被返回
         // (注意: head 是最后被调用的委派)
         public static Delegate Combine(Delegate tail, Delegate head);
         // 创建有委派数组表示的某个链
         // (注意: 入口 0 是 head 并将是最后被调用的委派)
         public static Delegate Combine(Delegate[] delegateArray);
         // 从链中删除某个委派匹配值的目标/方法。
         // 返回新的 head 并将是最后被调用的委派
         public static Delegate Remove(Delegate source, Delegate value);
      }
当你构造一个新的委派对象时,这个对象的_prev域被置为null,表示在链表中没有其它对象。为了将两个委派组合到某个链表中,可调用Delegate的静态方法Combine()来完成。例:
      Feedback fb1 = new Feedback(FeedbackToConsole);
      Feedback fb2 = new Feedback(FeedbackToConsole2);
      Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2);
      Feedback fb3 = new Feedback(appobj. FeedbackToConsole);
      fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
当然,我们也可以使用数组方式组合:
      Feedback[] fbArray = new Feedback[3];
      fbArray[0] = new Feedback(FeedbackToConsole);
      fbArray[1] = new Feedback(FeedbackToConsole1); 
      fbArray[2] = new Feedback(FeedbackToConsole2);
      Feedback fbChain = Delegate.Combine(fbArray);
编译器生成的派生自MulticastDelegate类的类定义中,包含:
      public void virtual Invoke( Object value, Int32 item, Int32 numItems)
      {      
         // 如果链表中还有委派,则应该首先调用它们
         if (_prev != null) _prev.Invoke(value, item, numItems);
         // 针对特定的目标对象调用回调方法
         _target.methodPtr(value, item, numItems);
       }
      调用某个委派对象导致其前一个委派被首先调用,当前一个委派返回时,其返回值被丢弃。所以你的应用程序代码将只能从链头的委派中接收返回值(即调用的最后一个回调用法)。
      委派对象一旦被创立,它就被认为是不能改变的,也就是说委派对象总是将它们的_prev域置为null,并且不会再变。当把某个委派对象组合到链中时,Combine在内部构造一个新的委派对象,它于源对象有着相同的_target 和 _methodPtr指针域。_prev域被置成链中旧的头。新委派对象的地址从Combine中返回。例:
      Feedback fb = new Feedback(FeedbackToConsole);
      Feedback fbChain = (Feedback) Delegate.Combine(fb, fb);
      // fbChain 引用有两个委派对象的链。其中一个对象与fb所指的对象相同。
      // 另一个对象由Combine构造,这个对象的_prev域引用fb,并且Combine
      // 返回对新对象的引用。
      // fb 和 fbChain 引用完全相同的对象吗?
      Console.WriteLine((Object) fb == (Object) fbChain);//输出:False
      // fb 和 fbChain 引用相同的回调目标/方法吗?
      Console.WriteLine(fb.Equals(fbChain)); //输出:True
      如何从链中删除一个委派呢?Remove的第一个参数是委派对象链的头,第二个参数是要从链中删除的委派对象。为了将委派对象从链中删除,必须先建立新的委派对象。
      在Remove的调用中,包含一个欲删除的委派对象。这个委派对象对它自己的_target和_methodPtr域进行初始化。Remove方法扫描链(fbChain所指的链),检查链中有没有委派对象与新创建的委派对象相等。要记住,由 Delegate 类实现的并经过重载的Equals 方法只比较_target和_methodPtr域并忽略_prev域。
      如果找到一个匹配的值,Remove 方法则通过固定前一个委派对象的_prev域从链中删除匹配的委派对象。
      C#编译器为委派类型的实例自动地提供 += 和 -= 操作的重载。这两个操作符分别调用:
      Delegate.Combine 和 Delegate.Remove。
      Feedback fb = new Feedback(FeedbackToConsole);
      fb += new Feedback(FeedbackToConsole); //组合
      // 调用链: FeedbackToStream 和 FeedbackToConsole 被调用
      if (fb != null) fb(...);
      fb -= new Feedback(FeedbackToConsole); // 从链中删除一个回调 
      // 调用链: FeedbackToStream 被调用
      if (fb != null) fb(...);
      委派类型的 Invoke 方法包含了对前一个委派进行(如果有的话)调用的代码,所以链表链中所有的项目都被调用。这种算法对于大多数遇到的情况都适合,但是它也有许多限制。
      例如,回调方法的返回值全都被忽略掉了,只留下最后一个。用这种简单的算法没有办法获得所有回调方法运行后的返回值。除此之外,这个算法还有一些局限性。例如,一旦对某个委派的调用丢出异常或长时间阻塞的话会发生什么情况呢?因为这个算法是连续地调用链中每个委派,如果对其中某个委派对象出了问题就会影响链中其它的委派获得调用。
      为了解决这个问题,MulticastDelegate类提供了一个实例方法:GetInvocationList,你可以用这个方法显式地调用链中每一个委派,而所使用的算法可以是任意的:
     public class MulticastDelegate
     {
        // 创建一个委派数组;其中每一项都是链中项目的克隆。
        // (注意: 入口 0 是链尾,通常它被首先调用)
        public virtual Delegate[] GetInvocationList();
      }
      GetInvocationList 方法操作某个委派链的引用并返回一个引用委派对象的数组,其遍历指定的链并克隆链中每个对象,将这些克隆对象添加到数组中。每个克隆都将其自己的_prev域置为null,这样每个对象被隔离,保证不会与其它的对象链搞混。

        .NET体系中的源程序安全问题
      在.NET平台上,代码以中间语言的形式运行,它是.NET众多优势的基础。但在独立桌面应用中,它给源代码的安全带来了威胁。
      在Visual Studio.NET(VS.NET)体系中,VB、Visual C++以及C#之类的编译器把源程序编译成MSIL。MSIL即Microsoft Intermediate Language,或Microsoft中间语言,它在执行之前被即时(Just-In-Time Compile,JIT)编译成为机器语言。但是,你可能还没有深入了解当你在VS中点击Build按钮时发生了什么事情,或者你私有的源代码和信息是否在偷窥的眼光面前安然无恙、当你把IL代码发布给客户时是否能够保证代码不被篡改。
      你必须搞清楚以下几个问题。首先,.NET是为客户机/服务器系统以及Web应用而设计的。软件开发正在逐渐向Internet以及基于客户机/服务器的应用发展,许多应用不再有传统风格的界面,而是提供类似浏览器的界面。.NET也同样追随着这个趋势。
      第二,在桌面应用中,我们无法保护以受管理的MSIL形式存在的代码,如果你觉得保护知识产权(即源代码)非常重要,那么.NET不适合桌面应用开发。虽然MSIL的承诺令人心动,虽然.NET平台和CLR(Common Language Runtime)很稳定,但从安全的角度来看,对于一个独立的桌面应用来说,这一切缺乏实际意义。在.NET中,作为一个VB程序员甚至是C#程序员,你只能编写受管理、不受保护的代码。
      由于存在这种限制,如果你要在桌面应用中保护代码,你必须使用非受管理的C++。保护知识产权唯一真正有效的方法是:用非受管理的C++组件封装代码,然后从.NET受管理代码中通过COM协作接口调用它。
      另外你还必须清楚的是,由于Active Server Pages.NET(ASP.NET)完全在服务器端运行,因此ASP.NET应用是安全的。实际上,这正是.NET最理想的境界——在受保护的服务器上运行代码,让代码远离任何想要研究它的人。ASP.NET把Web开发简化到了难以置信的程度,而Visual Basic.NET正是编写ASP.NET应用的优秀工具。

  中间语言
      传统的CPU利用寄存器和栈完成所有工作。CLR所提供的执行引擎只有一个栈,它的操作过程非常类似于一个逆波兰表示法计算器。如果某个过程调用具有多个参数,执行引擎将在发出调用之前把参数压栈。函数调用的返回值也通过栈传递。
      MSIL中的局部变量很容易识别,它们用.locals关键词声明。如果符号存在的话,你将看到变量名字;否则,你看到的将是V_1、V_2之类的变量:
      当我们运行编译器时,它生成的不是我们今天熟悉的执行文件,而是一个程序集(Assembly)。程序集是一个文件的集合,程序集中的文件可以作为单一整体进行部署。在当前的Windows体系中,我们可以把单个执行文件看成一个程序集。但从更严格的意义上来说,程序集聚合了执行文件和它的所有支持文件,包括DLL、图形、资源以及帮助文件。
      一般地,一个程序集至少由两个文件构成:执行部分,manifest(英文单词原意:载货清单,乘客名单)。manifest是程序集内所有文件的清单。程序集内的可执行部分又分开称为模块(Module)。从概念上说,模块对应着DLL或者EXE文件;除了父程序集所包含的元数据(Metadata)之外,每一个模块都包含元数据。程序集是当前可移植执行文件格式(Portable Executable,PE)的一个增强版本。
      一个程序集文件的开头是标准的PE头。文件内部包含了CLR头,CLR头的后面是把代码装入进程空间所必需的描述数据——即元数据。元数据为执行引擎提供了大量信息,其中包括:如何装载模块,需要哪些支持文件,如何装载支持文件,如何与COM以及.NET运行时环境交互。另外,元数据还描述了模块或者程序集所包含的方法、接口以及类。元数据所提供的信息使得JIT编译器能够编译并运行模块。同时,元数据暴露了有关应用的大量内部信息,使得从反汇编IL获取有价值的代码更加方便。
      使用.NET代码的核心问题在于受管理代码。受管理代码是专门为在CLR控制之下运行而编写的代码,它可以用VB.NET、C#以及C++等语言创建,但C++是唯一能够创建.NET平台非受管理代码的语言。
      MSIL的有一个优点涉及到.NET的两个V:Validation(检验),Verification(核查)。检验是对模块进行的一系列检查,确保元数据、MSIL代码以及文件格式的一致性。不能通过这些检查的代码可能导致执行引擎或者JIT编译器崩溃。一旦模块通过了检验,则代码是正确的且可以开始运行。
      JIT编译器把MSIL代码转换成机器代码时对代码进行核查,它是对元数据进行复查,保证程序不会访问它不具有相应许可的内存或其他资源。经过核查的代码是类型安全的(Type-Safe)代码。这种核查即使是在程序被直接编译成机器代码的时候也要进行,但除非由JIT编译器进行核查,否则这种核查不是100%精确无误,因为核查结果依赖于来自其他程序集的元数据。如果把源程序直接编译成机器代码,我们面临着这样一种危险:在目标机器上的其他程序集发生了变化,从而导致程序不再类型安全。
      使用JIT编译器保证了检验和核查是对所有相关程序集的当前版本进行。这些操作确保执行程序总是类型安全,程序总是以合适的安全许可运行。你可以用.NET SDK的

 反向工程
      当程序集以MSIL而不是机器代码的形式发布时,最令人关心的问题应该就是安全。正如前面所介绍的,程序集包含了关于包里面所有模块的manifest以及详细描述各个模块的元数据。.NET SDK 提供了一个名为ILDASM的工具,它是一个IL反汇编程序,能够从模块反汇编出IL代码以及应用程序中各个模块的元数据说明。
      人们已经认识到了这个问题,一个常见的反驳意见是:在现实中,应用的规模很大,IL反汇编输出结果的规模将超过可以忍受的限度。但是,它可能使一个业余爱好者望而却步,却不能阻止一个真正对代码感兴趣的人。实际情况是:与机器代码的反汇编结果相比,ILDASM的反汇编结果要容易阅读得多,任何对此感兴趣的组织都能够从IL反汇编结果了解到大量有关应用的信息。
      按照Microsoft的意见,要保证企业机密安全,我们应该把所有包含企业机密的模块放到受保护的服务器上。对于ASP.NET客户机/服务器应用来说这没问题,但对于标准的桌面应用来说它行不通。那么,如何才能对知识产权进行保护呢?MSIL汇编程序文档提到了一个命令行参数/owner:
ilasm ... /owner
ilasm ... /owner=fergus

结束语
      如果你是一个桌面应用的供应商,你清楚自己应该怎么做。你可以用非受管理的C++编写代码,然后从受管理的VB调用它。用这种方法设计应用,你能够确信代码的安全。然而,如果你是一个第三方供应商,而且准备在组件中用非受管理的代码替代受管理的代码,那么,你是在强迫用户放弃.NET的优势,重新让他们面对他们今天所面临的问题。受管理代码能够防止对应用本身或者其他应用所使用的内存空间进行破坏性操作,对受管理代码的支持正是.NET吸引人的原因之一。某些用户可能会查看受管理代码的IL程序,甚至还有可能分析应用的算法实现,如果不能正确地认识.NET的优势所在,第三方供应商可能会为了防止用户分析代码而拒绝用受管理代码编写各种软件部件。
      

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值