• 挂掉.NET 1


    对应到.NET上的话,
    第一点基本上就映射到P/Invoke的使用了。如果被P/Invoke的native code里有非常糟糕的错误而且不使用SEH,那CLR什么办法也没有,只能让程序crash了。

    第二点是关于操纵VM内部实现用到的指针。各种JVM实现里在不同位置暴露了一些指针(即便是Compressed Oops那也是指针),改变它们的值确实能达到crash的效果,虽然如果更进一步能它它们改成“有意义”的值的话就能更有效的操纵破坏的具体行为。
    CLR里也有许多看起来很无辜的东西实际上是指针来的(注意我是说CLR不是CLI)。一个典型的例子是Type.TypeHandle属性,在CLR里它实际上就是指向类型的MethodTable的指针。通过它我们可以找到很多关于类型的“裸”信息。“裸”是指CLR内部的实现细节,本来不应该暴露出来的部分)。还有一个典型的例子是.NET的类型安全函数指针,委托。下面会看看委托的例子。
    要操纵VM内部的指针,势必要通过反射去获取或设置一些私有变量的值。这种操作一般都会受到VM的安全管理器监管,在没有足够权限的情况下无法执行。所以其实也不算危险……不,应该说原本用native code的话就有这种危险了,用了VM并没有变得更危险。

    第三点是说VM自身的实现有bug。嗯这种状况常有,像先前我就看到HotSpot的JIT有bug挂掉了。CLR小组也没少遇到内部发生内存管理错误的问题,组里有专人盯着这种问题在修。如果发现这样的bug并有意利用的话,也能有效让VM挂掉,甚至进一步做别的事情……呵呵

    ===========================================================================

    .NET的委托,在不考虑多播(multicast)状况时,完成调用所需要的Delegate类上最关键的3个成员是_target、_methodPtr和_methodPtrAux。其中只有_target是以Delegate.Target属性的形式公开出来的。看看它们都有什么用:

    _target:委托调用的目标对象。
    .NET的委托是类型安全的,不但指在构造委托实例时会检查其类型与目标方法的signature是否匹配,也指委托能够捕获目标对象的引用,进而能够由其得到相关的类型和方法的元数据,以供执行引擎监管类型的安全性。
    在CLR的实现中,_target可能有两种情况:
    1、如果委托指向的方法是成员方法,那么_target就会是指向目标方法所属的对象实例的指针;
    2、如果委托指向的是静态方法,或者是涉及native方法,那么_target会指向委托实例自身。
    有趣的是,虽然指向静态方法时_target指向委托实例自身,但Delegate.Target却会返回null。

    _methodPtr:委托调用的目标方法的指针。
    这个是“函数指针”的真面目,跟C里的函数指针没什么两样。
    它的值也分两大种情况:
    1、如果委托指向的方法是成员方法,那么_methodPtr就可能指向一个JIT stub(假如创建委托时目标方法尚未被JIT),或者可能是直接指向目标方法JIT后的地址;
    2、如果委托指向的方法是静态方法,那么_methodPtr指向的是一个stub,去掉原本调用时隐藏的第一个参数(_target),然后调用_methodPtrAux。这个stub是所有signature相同的委托共享的。
    如果涉及native方法的话我还没弄清楚具体是什么状况 =v=

    _methodPtrAux:委托调用的目标方法的第二个指针。
    联系前两个成员的介绍,这个也不例外分两种情况:
    1、如果委托指向的是成员方法,那么_methodPtrAux就是null(0)。Delegate.Target属性实际的实现是_methodPtrAux.IsNull() ? _target : null,可以看到目标是成员方法与否的影响。
    2、如果委托指向的是静态方法,那么_methodPtrAux可能指向类似JIT stub的东西,该stub在多次调用后可能会被改写为jmp到实际调用目标方法;也可能一开始就指向目标方法JIT后的地址。
    (CLRv2中,“多次”是3次;采取哪个版本的_methodPtrAux取决于创建委托实例所在的方法在被JIT编译时,目标方法是否已经被JIT编译)

    抽象的描述还是让人摸不着头脑,来看看代码例子:

    C#代码 复制代码 收藏代码
    1. using System;   
    2. using System.Reflection;   
    3.   
    4. namespace TestCLR2Crash {   
    5.     static class Program {   
    6.         static void Main( string[ ] args ) {   
    7.             Func<intint> iden = x => x;   
    8.             Func<intint> succ = x => x + 1;   
    9.             var methPtrAuxInfo = typeof( Func<intint> ).GetField( "_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance );   
    10.             var succPtrAux = ( IntPtr ) methPtrAuxInfo.GetValue( succ );   
    11.             methPtrAuxInfo.SetValue( iden, succPtrAux );   
    12.             Console.WriteLine( iden( 0xBEEF ).ToString( "X" ) ); // BEF0   
    13.         }   
    14.     }   
    15. }  
    using System;
    using System.Reflection;
    
    namespace TestCLR2Crash {
        static class Program {
            static void Main( string[ ] args ) {
                Func<int, int> iden = x => x;
                Func<int, int> succ = x => x + 1;
                var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance );
                var succPtrAux = ( IntPtr ) methPtrAuxInfo.GetValue( succ );
                methPtrAuxInfo.SetValue( iden, succPtrAux );
                Console.WriteLine( iden( 0xBEEF ).ToString( "X" ) ); // BEF0
            }
        }
    }


    先注意一些C#的实现细节。Main里的iden与succ所指向的lambda都没有捕获任何自由变量,所以由C#编译器先改写生成对应的私有静态方法。这样,iden与succ就属于“指向静态方法的委托”的情况,可以留意一下相应的_target、_methodPtr与_methodPtrAux的表现。特别的,iden与succ的_target成员指向各自自身;它们的_methodPtr都指向同一个stub,用于剥离第一个隐藏参数并调用_methodPtrAux;由于Main()方法被JIT的时候,两个lambda对应的静态方法尚未被JIT,所以iden与succ的_methodPtrAux各自指向不同的stub(而不是直接指向实际调用目标方法)。

    在代码中,我们把succ的_methodPtrAux提取出来,并设置到iden对应的域里。然后在调用iden时,可以看到实际被调用的是succ指向的那个lambda。

    既然能把函数指针改到一个有效的函数地址上,那要是改为null的话呢?

    C#代码 复制代码 收藏代码
    1. using System;   
    2. using System.Reflection;   
    3.   
    4. namespace TestCLR2Crash {   
    5.     static class Program {   
    6.         static void Main( string[ ] args ) {   
    7.             Func<intint> iden = x => x;   
    8.             var methPtrAuxInfo = typeof( Func<intint> ).GetField( "_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance );   
    9.             methPtrAuxInfo.SetValue( iden, IntPtr.Zero );   
    10.             Console.WriteLine( iden( 0xBEEF ).ToString( "X" ) );   
    11.         }   
    12.     }   
    13. }  
    using System;
    using System.Reflection;
    
    namespace TestCLR2Crash {
        static class Program {
            static void Main( string[ ] args ) {
                Func<int, int> iden = x => x;
                var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_methodPtrAux", BindingFlags.NonPublic | BindingFlags.Instance );
                methPtrAuxInfo.SetValue( iden, IntPtr.Zero );
                Console.WriteLine( iden( 0xBEEF ).ToString( "X" ) );
            }
        }
    }


    我们就让CLR挂掉而出现AV(access violation)了:

    引用
    Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.


    可惜CLR的实现比较严谨,AV也还是被默认的异常处理捕捉到了。不过如果指向什么别的地方,说不定就能在触发AV前先干点好事了,呵呵。

    再次注意到像这样操纵VM内部的指针需要足够的安全权限才行,否则通过反射也无法像这样修改私有变量的值。所以并不会很不安全,可以放心。

    说真的,即便写个会爆栈的程序,CLR也会扔出类似的错误信息:

    引用
    Process is terminated due to StackOverflowException.


    改委托内部的函数指针不够好玩……

    ===========================================================================

    回复中cescshen同学问了个有趣的问题,说为什么改变_target也可以改变实际被调用的对象。我把我的回帖复制上来~
    以下内容都是以PC上的32位x86的CLR,版本2.0.50727.3082为前提的讨论。

    cescshen 写道
    发错,这儿不能删自己的留言。。

    C#代码 复制代码 收藏代码
    1. var methPtrAuxInfo = typeof( Func<intint> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );   
    var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance ); 

    改成这样的话,也能出结果,这个怎么回事?


    如果你说的不是遇到了错误,而是看到修改_target后iden的行为变成了succ的,那是因为在_methodPtr所指向的那个stub里,代码是这样的:

    X86 asm代码 复制代码 收藏代码
    1. mov         eax,ecx         // 把第一参数(_target)复制到EAX   
    2. mov         ecx,edx         // 把原本的第二参数(0xBEEF)变为第一参数   
    3. add         eax,10h         // 把_target._methodPtrAux的地址设到EAX   
    4. jmp         dword ptr [eax] // 间接调用EAX,也就是调用_target._methodPtrAux  
    mov         eax,ecx         // 把第一参数(_target)复制到EAX
    mov         ecx,edx         // 把原本的第二参数(0xBEEF)变为第一参数
    add         eax,10h         // 把_target._methodPtrAux的地址设到EAX
    jmp         dword ptr [eax] // 间接调用EAX,也就是调用_target._methodPtrAux


    注意到CLR里JIT编译的代码的calling convention是类似fastcall的,头两个参数分别位于ECX和EDX。在调用iden的时候,代码是这样的:

    X86 asm代码 复制代码 收藏代码
    1. mov         ecx,edi                 // 把iden的引用从EDI复制到ECX   
    2. mov         edx,0BEEFh              // 0xBEEF复制到EDX作为第二参数   
    3. mov         eax,dword ptr [ecx+0Ch] // 把iden._methodPtr复制到EAX   
    4. mov         ecx,dword ptr [ecx+4]   // 把iden._target复制到ECX作为第一参数   
    5. call        eax                     // 调用_methodPtr  
    mov         ecx,edi                 // 把iden的引用从EDI复制到ECX
    mov         edx,0BEEFh              // 0xBEEF复制到EDX作为第二参数
    mov         eax,dword ptr [ecx+0Ch] // 把iden._methodPtr复制到EAX
    mov         ecx,dword ptr [ecx+4]   // 把iden._target复制到ECX作为第一参数
    call        eax                     // 调用_methodPtr



    知道从_methodPtr到_methodPtrAux的过程之后,就可以理解为什么改变_target的值也足以改变指向静态方法的委托的行为:因为关键的_methodPtrAux是通过_target来引用的。在正常情况下,_target就指向委托自身,所以没有问题;而改变了_target的值之后,实际被调用的_methodPtrAux就跟着一起变了。

  • 相关阅读:
    扩欧(exgcd讲解)
    Django组件之forms
    Django组件之用户认证
    Django之中间件
    Django之cookie与session
    Django组件之分页器
    Django之Ajax
    Django之模型层2
    Django之模型层
    Django之模板层
  • 原文地址:https://www.cnblogs.com/qianyz/p/2407765.html
Copyright © 2020-2023  润新知