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

要让CLR挂掉的话……

阅读更多
(Disclaimer:如果需要转载请先与我联系。
作者:RednaxelaFX -> rednaxelafx.iteye.com)

系列文章:
要让CLR挂掉的话……
要让CLR挂掉的话(第二弹)……

前几天跟浩飞老兄闲聊的时候,聊到说一个不知道什么地方在面试人的时候,如果面试者说自己精通Java,他们就出题考面试者如何让JVM挂掉。这种面试方式或许是比较激进,不过倒也可以考考别人对特定JVM的实现的认识。
于是在爆栈上有这么一帖:How do you crash a JVM?。跟帖中一些同学的观点一样,我也不认为爆栈或者爆堆能算得上是“crash”,因为JVM还能正确捕捉到错误,并且执行合适的异常处理。真正的“crash”应该是连正常的异常处理都没起作用,直接就出crash log了;要是能连出crash log的步骤都破坏掉那就更彻底了。
爆栈帖里有人建议说:
ralfs 写道
1. Use JNI and crash in the native code.
2. If no security manager is installed you can use reflection to crash the VM. This is VM specific, but normally a VM stores a bunch of pointers to native resources in private fields (e.g. a pointer to the native thread object is stored in a long field in java.lang.Thread). Just change them via reflection and the VM will crash sooner or later.
3. All VMs have bugs, so you just have to trigger one.

都是些有趣的建议……

对应到.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
        }
    }
}

先注意一些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" ) );
        }
    }
}

我们就让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 写道
发错,这儿不能删自己的留言。。

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

注意到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


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

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

爆栈帖里有一个回复可以mark一下:
eckes 写道
On Linux/Unix you can easyly make a JVM crash by sending it a Signal to the running process. Note: you should not use "SIGSEGV" for this, since Hotspot catches this signal and rethrows it as a NullPointerException in most places. So it is better to send a SIGBUS for example.
分享到:
评论
10 楼 lindexi-gd 2016-07-08  
我想转载blog.csdn.net/lindexi_gd
9 楼 cescshen 2009-09-03  
var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );   
var succPtrAux = methPtrAuxInfo.GetValue( succ ); 

嗯, 我是指改变_target后, iden的行为变成了succ的, 我没有转IntPtr, 所以没有InvalidCastException. 解答真及时啊
8 楼 RednaxelaFX 2009-09-02  
cescshen 写道
发错,这儿不能删自己的留言。。

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         // 把原本的第二参数变为第一参数
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


知道从_methodPtr到_methodPtrAux的过程之后,就可以理解为什么改变_target的值也足以改变指向静态方法的委托的行为:因为关键的_methodPtrAux是通过_target来引用的。在正常情况下,_target就指向委托自身,所以没有问题;而改变了_target的值之后,实际被调用的_methodPtrAux就跟着一起变了。
7 楼 RednaxelaFX 2009-09-02  
night_stalker 写道
想到个把内存搞成筛状的做法:
1.分配一块 n字节 的内存 m1 (如 new Byte[n])
2.分配一块小小的内存 m2    (如 new Byte[1])
3.释放 m1                  (如退出作用域或者弱引用让它可以被 GC 回收)
4.但保存 m2                (如 somelist.appen(m2))
5.n += 1,回到 1 继续。

最后某些 VM 会总空间很足够,就是分不出内存块来。但某些 VM 不会。

---

任何有计数的地方都有可能 overflow, 十万个类或者十万个变量?

---

感觉搞 channel 和反射也比较容易搞 crash,还有大量几乎没人用的 API 也是弱点。

---

靠 IL 搞 crash 就更容易了,当然对于我们不熟 IL 的太不容易了 ……

如果不用unverifiable code的话,MSIL很难让CLR“挂掉”,因为CLR在执行某个方法之前会对里面的MSIL进行校验,如果是有问题的MSIL,那还没到执行的那步就已经被踢了。

内存的那个,你想说的“保存m2”在.NET术语里或许是“把m2给pin住”。
pin住的话,m2就无法移动了,因而会造成内存碎片化。如果对象大小不太大(没超过85000字节)的话,对象是在一般的GC堆上分配空间,而它会在碎片比例开始影响性能时对堆进行压缩;pin住的对象无法移动,所以会影响GC对堆的压缩。如果没有pin住的对象,那CLR的一般的GC堆是不会出现严重的碎片化的。
如果对象大小超过了那个阈值,那么会分配在LOH(大对象堆)上。这个堆是不压缩的,所以会出现碎片化的问题。要故意构造一个让它碎片化的程序,只要不停的在85000字节以上的大小递增的创建对象,中间夹些刚比85000字节大一些的对象,然后留住中间夹的那些,就足够了……
6 楼 RednaxelaFX 2009-09-02  
cescshen 写道
发错,这儿不能删自己的留言。。

var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance ); 

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

帮你把错误的留言删了~ ^ ^
你说的“可以出结果”是指什么?是指iden能按照原本正常的行为执行,还是指看到了错误的结果,还是指出错让CLR退出?

如果是说看到了错误,那……莫非你测试的代码是这样的?
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( "_target", BindingFlags.NonPublic | BindingFlags.Instance );
            var succPtrAux = ( IntPtr ) methPtrAuxInfo.GetValue( succ );
            methPtrAuxInfo.SetValue( iden, succPtrAux );
            Console.WriteLine( iden( 0xBEEF ).ToString( "X" ) ); // BEF0
        }
    }
}

这样的话,在var succPtrAux = (IntPtr) methPtrAuxInfo.GetValue(succ);这行自然就挂了,因为_target的实际类型是Func<int, int>,而这行代码指定要转换到IntPtr,无法转换,所以抛出InvalidCastException。这是正常现象,都还没涉及到跟指针相关的问题……

不知道你遇到的是不是这个情况呢?
5 楼 night_stalker 2009-09-02  
想到个把内存搞成筛状的做法:
1.分配一块 n字节 的内存 m1 (如 new Byte[n])
2.分配一块小小的内存 m2    (如 new Byte[1])
3.释放 m1                  (如退出作用域或者弱引用让它可以被 GC 回收)
4.但保存 m2                (如 somelist.appen(m2))
5.n += 1,回到 1 继续。

最后某些 VM 会总空间很足够,就是分不出内存块来。但某些 VM 不会。

---

任何有计数的地方都有可能 overflow, 十万个类或者十万个变量?

---

感觉搞 channel 和反射也比较容易搞 crash,还有大量几乎没人用的 API 也是弱点。

---

靠 IL 搞 crash 就更容易了,当然对于我们不熟 IL 的太不容易了 ……
4 楼 cescshen 2009-09-02  
发错,这儿不能删自己的留言。。

var methPtrAuxInfo = typeof( Func<int, int> ).GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance ); 

改成这样的话,也能出结果,这个怎么回事?
3 楼 lwwin 2009-09-02  
我倒觉得有时候程序crash挺好了…………

经常会因为弄得太好了,结果没办法捕捉错误了……
2 楼 RednaxelaFX 2009-09-02  
iaimstar 写道
  public static void main(String[] args) {
    throw new RuntimeException();
  }

我看到了这个 好搞笑
有人给打成-2了

那是……太直了就没有含蓄美朦胧美了 =v=
1 楼 iaimstar 2009-09-02  
  public static void main(String[] args) {
    throw new RuntimeException();
  }

我看到了这个 好搞笑
有人给打成-2了

相关推荐

Global site tag (gtag.js) - Google Analytics