对应到.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编译)
抽象的描述还是让人摸不着头脑,来看看代码例子:
- 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
- }
- }
- }
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的话呢?
- 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" ) );
- }
- }
- }
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)了:
可惜CLR的实现比较严谨,AV也还是被默认的异常处理捕捉到了。不过如果指向什么别的地方,说不定就能在触发AV前先干点好事了,呵呵。
再次注意到像这样操纵VM内部的指针需要足够的安全权限才行,否则通过反射也无法像这样修改私有变量的值。所以并不会很不安全,可以放心。
说真的,即便写个会爆栈的程序,CLR也会扔出类似的错误信息:
改委托内部的函数指针不够好玩……
===========================================================================
回复中cescshen同学问了个有趣的问题,说为什么改变_target也可以改变实际被调用的对象。我把我的回帖复制上来~
以下内容都是以PC上的32位x86的CLR,版本2.0.50727.3082为前提的讨论。
- var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );
var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );
改成这样的话,也能出结果,这个怎么回事?
如果你说的不是遇到了错误,而是看到修改_target后iden的行为变成了succ的,那是因为在_methodPtr所指向的那个stub里,代码是这样的:
- mov eax,ecx // 把第一参数(_target)复制到EAX
- mov ecx,edx // 把原本的第二参数(0xBEEF)变为第一参数
- add eax,10h // 把_target._methodPtrAux的地址设到EAX
- 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的时候,代码是这样的:
- 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
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就跟着一起变了。