关闭

要让CLR挂掉的话……

123人阅读 评论(0) 收藏 举报

http://rednaxelafx.iteye.com/blog/460893

(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 injava.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编译)

抽象的描述还是让人摸不着头脑,来看看代码例子:
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. }  

先注意一些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. }  

我们就让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 );   

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

如果你说的不是遇到了错误,而是看到修改_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  

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


知道从_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.


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

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

前一篇的后半段一样,以下内容都是以PC上的32位x86的CLR、版本2.0.50727.3082,以及Windows XP SP3为前提的讨论。

前一篇提到了通过改变委托中的指针来改变实际的调用目标。修改委托实例中的_target、_methodPtr、_methodPtrAux这三个成员,都能够改变跳转目标;特别是后两个,它们的类型是IntPtr,可以构造出任意数值的指针设置进去,那样就可以跳转到任意目标了。
但只能指定目标地址,却不能随意控制目标里的代码,显然还不够好玩。如果要跳转的目标是托管方法,那构造一个正常的委托就够了。如果能在不使用P/Invoke也不使用unsafe code的条件下在C#程序里执行一小块自定义的native code就好玩多了。先前的两篇日志(这里这里)我提到在内存里生成native code并执行并不是件难事。那么在CLR上的C#也能做到么?

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

要玩怎样的代码?

如果能执行任意native code的话,不得不说可玩的东西就多了。例如说把整个调用栈给乱搅一通、把SEH链全都破坏掉;或者……嗯还是别想那么可怕的玩法了,我还不想把自己的系统弄垮。等什么时候我再装个用了就扔的虚拟机镜像再试可怕的玩法……

还是跟前面一样,写段类似HelloWorld的代码,往标准输出流写句话,表明“可以做”就算了。想用System.Console.WriteLine()会有点不爽,因为它要接收CLR对象(的引用)为参数,而我不想费事去在自己的native code里去找出System.String的type token、调用CORINFO_HELP_NEWSFAST创建新实例、调用构造器之类的一大堆麻烦事。我就想把一个C风格的字符串输出而已。那么,就不用.NET标准库里的方法了,干脆直接用Win32 API,简单省事,WriteConsole()函数正好够用。调用Win32函数时也懒得通过P/Invoke,而是在native code里直接call过去。

要用Win32 API,首先得确保需要的DLL已经被加载到当前进程中。CLR为了自身的正常运行,本来就需要加载很多模块。可以看看一个HelloWorld式的托管程序都加载些什么模块进来。

C#代码  收藏代码
  1. using System;  
  2.   
  3. static class Program {  
  4.     static void Main(string[] args) {  
  5.         // block the program so that we could easily attach a debugger  
  6.         var name = Console.ReadLine();  
  7.         Console.WriteLine("Greet me, {0}", name);  
  8.     }  
  9. }  

除了这个exe本身之外,可以看到加载进来的模块有:
引用
ADVAPI32
COMCTL32
COMCTL_1
GDI32
IMM32
KERNEL32
LPK
MSCOREE
MSCORJIT
MSCORLIB
MSCORWKS
MSVCR80
MSVCRT
OLE32
RPCRT4
SECUR32
SHELL32
SHLWAPI32
USER32
USP10

光是一个HelloWorld就加载了这么多DLL,可以用的函数那就多了 XD
要调用的WriteConsoleA()函数位于Kernel32.dll中,在列表里可以找到,没问题。其实就没什么Win32程序是不加载Kernel32.dll的吧 =v=

怎么获取Win32 API的函数地址呢?如果是在C里,那不用管函数地址,引入windows.h或相关头文件后正常用那些函数就行。要不然LoadLibrary()得到模块句柄然后GetProcAddress()得到函数地址也行。前者在C#里固然是不行(即使用P/Invoke然后用个委托指向包装函数,得到的地址也是stub的地址而不是底下实际目标函数的地址);后者有自举困难——LoadLibrary()的地址从哪儿来?

所以干脆ad-hoc点,既然是玩嘛就先别那么麻烦,先弄出能演示的版本再说。用别的办法找到需要用的函数的地址,然后硬编码到我们生成的native code里就算了。因为DLL有默认的加载地址,只要一个进程里加载的DLL没有地址冲突,它们所在的位置就是可预测的,其中函数的位置也是可预测的。当然,我是在XP上玩,在Vista之后有ASLR,地址就不好预测了……但如果按照下文的方法保存生成的native code的话,程序在遇到ASLR前先就被DEP干掉了。

OK,废话那么多,我想达到的效果要是用C直接写会是怎样呢?
C代码  收藏代码
  1. #include <windows.h>  
  2.   
  3. int main() {  
  4.     HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);  
  5.     WriteConsole(hStdout, "Greetings from generated code!\n", 31, NULL, NULL);  
  6.       
  7.     return 0;  
  8. }  

很简单对吧?基本对应的机器码和汇编,后面还会用到:
X86 asm代码  收藏代码
  1. 55              push ebp  
  2. 8BEC            mov  ebp,esp  
  3. 6A F5           push -0B                         ; /DevType = STD_OUTPUT_HANDLE  
  4. B8 D92F817C     mov  eax,KERNEL32.GetStdHandle   ; |  
  5. FFD0            call eax                         ; \GetStdHandle  
  6. 6A 00           push 0                           ; /pReserved = NULL  
  7. 6A 00           push 0                           ; |pWritten = NULL  
  8. 6A 1F           push 1F                          ; |CharsToWrite = 1F (31.)  
  9. E8 00000000     call <&next_instruction>         ; |  
  10. 830424 10       add  dword ptr ss:[esp],10       ; |Buffer  
  11. 50              push eax                         ; |hConsole  
  12. BA 5DCC817C     mov  edx,KERNEL32.WriteConsoleA  ; |  
  13. FFD2            call edx                         ; \WriteConsoleA  
  14. 8BE5            mov  esp,ebp  
  15. 5D              pop  ebp  
  16. C3              ret  

我准备把要输出的字符串紧接在代码后面。注意这里用了之前介绍过的一个技巧,通过call指令来获取当前IP(指令指针)的值,并由此计算出要输出的字符串的地址。其实不用这个技巧也可以的,毕竟我已经知道代码的起始地址了。不过玩嘛,呵呵~
我用call r32指令而不是call imm32指令来调用那两个Win32函数,是因为前者的r32里装的是虚拟内存的绝对地址,而后者的imm32里装的是相对下一条指令的偏移量;我懒得在生成代码的时候去计算偏移量所以用前者了。
另外还要注意,Win32 API的calling convention是WINAPI,也就是__stdcall,是由被调用方来清理栈的。

其实要让CLR无声无息的就停掉,在native code里调用TerminateProcess()应该也行。这次的例子还是专于HelloWorld好了 =v=

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

如何存放生成的native code?

之前用C来演示运行时操纵代码的时候,是把native code“生成”到malloc出来的一块堆空间上的。在C#里我要怎么找到可写可执行的一块放代码的空间呢?

前一篇的后半段中我介绍了改变委托的_target成员也可以改变最终被调用的目标。留意到_target的类型是System.Object,也就是说任何对象都可以放在里面。如果构造一个委托实例时,它是指向静态方法的,那么它的_methodPtr成员就会指向那个特定的stub:(stub会根据委托类型的参数个数不同而不同,下面是接收一个参数的委托的情况)
X86 asm代码  收藏代码
  1. mov         eax,ecx          ; 把第一参数(_target)复制到EAX  
  2. mov         ecx,edx          ; 把原本的第二参数变为第一参数  
  3. add         eax,10h          ; 把_target._methodPtrAux的地址设到EAX  
  4. jmp         dword ptr [eax]  ; 间接调用EAX,也就是调用_target._methodPtrAux  

这个stub完全不理会_target到底是什么类型的,直接从偏移量0x10的地方取出一个DWORD,然后就间接跳转过去了。正常情况下_target指向委托自身,那么在偏移量0x10的地方就是_methodPtrAux成员,整个逻辑就是对的。那要是狸猫换太子,放点什么别的东西进去当作_target呢?

C#里,引用类型的默认内存布局是LayoutKind.Auto,值类型的默认内存布局是LayoutKind.Sequential,而我们现在需要的是在一个确定的偏移量保持跳转目标的地址。给类型指定LayoutKind.Explicit可以达到目的,不过其实有更简单的办法,连特殊类型都不需要声明——直接用数组就行了嘛。数组里的元素肯定是按顺序保存的。

CLR里,一个最简单不过的int[]在内存中的布局如下:(括号中数字表示距离数组起始地址的偏移量)
-----------------------
|      SyncBlk索引     | (-4)
-----------------------
| 指向MethodTable的指针 | (+0)
-----------------------
|    数组长度 Length    | (+4)
-----------------------
|     下标为0的元素      | (+8+4*0)
-----------------------
|     下标为1的元素      | (+8+4*1)
-----------------------
|         ...          |
-----------------------
|     下标为n的元素      | (+8+4*n)
-----------------------
|         ...          |
-----------------------

而一个委托实例的开头部分在内存中的布局是:
-----------------------
|      SyncBlk索引     | (-4)
-----------------------
| 指向MethodTable的指针 | (+0)
-----------------------
|       _target       | (+4)
-----------------------
|     _methodBase     | (+8)
-----------------------
|      _methodPtr     | (+12)
-----------------------
|    _methodPtrAux    | (+16)
-----------------------
|   _invocationList   | (+20)
-----------------------
|  _invocationCount   | (+24)
-----------------------

那么只要用一个int[](或者uint[]),在下标为2的地方放一个数字,然后把该数组设为某个委托的_target,那就……嘿嘿。

在Windows XP上,DEP还没有对所有程序默认开启,所以基本上在堆上申请到的空间都是可写可执行的。CLR里的托管数组都在堆上分配空间,可以把native code“生成”到数组里保存着。不过Vista和Windows 7上DEP默认对所有程序都启用,而不通过VirtualAlloc()或者VirtualProtect()就没办法申请到可写可执行的空间,所以下面的办法在这些新的系统上运行会看到AccessViolationException。

结合我们需要在特定偏移量保存伪装的_methodPtrAux的需要,我们需要构造的int[]或者uint[]数组应该像这样:
-----------------------
|      SyncBlk索引     | (-4)
-----------------------
| 指向MethodTable的指针 | (+0)
-----------------------
|    数组长度 Length    | (+4)
-----------------------
|           0          | (+8+4*0==8,下标为0)
-----------------------
|           0          | (+8+4*1==12,下标为1)
-----------------------
|   假的_methodPtrAux   | (+8+4*2==16,下标为2)
-----------------------
|   生成的native code   | (+8+4*3==20 ...)
|         ...          |
-----------------------

其中,在下标为2的地方放置“假的_methodPtrAux”;该值应该等于下标为3的地址,好让委托调用到“生成”的native code。
于是又有问题了:我们该如何得到数组的地址?

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

如何获得对象的地址?

可能会有人想到用对象的hashcode来找出对象的地址。让我们笼统的分析一下其合理性。

Java的java.lang.Object和.NET的System.Object都支持获取对象的hashcode。如果一个对象在“活着”的时候不会被移动,则其起始地址不会发生改变;对象间不应该有交叠,所以用对象地址直接作为hashcode是一种很直观的实现。事实上Android里的Dalvik虚拟机就是这样实现Object.hashCode()的,详细可查看Dalvik源码vm/native/java_lang_Object.c中的Dalvik_java_lang_Object_hashCode()和InternalNative.c中的dvmGetObjectHashCode()。
C代码
  1. /* 
  2.  * Return the hash code for the specified object. 
  3.  */  
  4. u4 dvmGetObjectHashCode(Object* obj)  
  5. {  
  6.     return (u4) obj;  
  7. }  

注意到Dalvik的GC是典型的标记-清除(mark-and-sweep)式,不会移动堆中的对象。

也可以留意一下Apache Harmony里的其中一种hashcode计算方式,第16页:Design a Product-Ready JVM for Apache Harmony

也有一些JVM的实现会选择以对象地址为源通过位移、异或等运算来计算hashcode,这种情况下要从hashcode反推回来原本的地址就有点困难了。
Mono在使用不移动对象的GC时,采用的hashcode算法来自Thomas Wang,Address Based Hash Function
其算法实现是:
C代码
  1. uint32 address_hash(char* addr)  
  2. {  
  3.   register uint32 key;  
  4.   key = (uint32) addr;  
  5.   return (key >> 3) * 2654435761;  
  6. }  


那要是GC会移动对象呢,例如说采用了拷贝式收集器或者压缩式收集器的话?一个办法是可以拿对象第一次被分配的地址为hashcode的源,另一个办法是干脆无视对象的地址,用别的办法来得到能够区分对象身份的值;还有些别的办法是上面两种的混合。Xiao-FengObject hashcode implementation一帖中描述了Apache Harmony实现hashcode的三种方案,可以参考阅读一下。

CLR中System.Object.GetHashCode()并不返回对象的地址,所以很可惜不能从这里挖出点指针来玩玩。

---------------------------------------------------------------------------

然后可能会有人想到像C那样用union来骗过类型系统。
C代码
  1. #include <stdio.h>  
  2.   
  3. typedef union tagMyUnion {  
  4.     int i;  
  5.     float f;  
  6. } MyUnion;  
  7.   
  8. int main() {  
  9.     MyUnion u;  
  10.     int i;  
  11.       
  12.     u.f = 2.0f;                                 // 0x40000000  
  13.     i = u.i;                                    // 0x40000000  
  14.     u.i = i + (1 << 23);                        // 0x40800000  
  15.     printf("%f\n", u.f);                        // 4.000000  
  16.     printf("%d, 0x%08x\n", i, i);               // 1073741824, 0x40000000  
  17.     printf("%d, 0x%08x\n", (int)u.f, (int)u.f); // 4, 0x00000004  
  18.       
  19.     return 0;  
  20. }  

在C里,要直接把float类型的值的底层表示“看作”int,光靠类型转换是不行的,因为编译器会在转换的地方插入类似inttofloat()的函数,把底层表示从单精度浮点数格式改变为二的补码整数格式。以前专门的浮点数运算器不普及时,程序员经常自己用整数运算去模拟浮点数运算,需要在不改变底层表示的前提下操作。怎么办呢?像上面那样用union就可以做到。通过union,任意等宽度的类型间都可以做不改变底层表示的转换,跟后来C++的reinterpret_cast作用一样。

CLR里的引用用指针的形式来实现,而指针以直接记录地址的形式来实现。(注意:引用、指针和地址是三个在不同抽象层次上的相关概念,不应该把它们看作同一抽象层次上的概念。)
虽然C#中没有reinterpret_cast,但有模拟C的union的结构:显式指定内存布局的struct。如果可以在C#里实现一个union,把IntPtr值和Object引用保存在同一位置,不就可以提取到对象的地址了吗?于是:
C#代码
  1. using System;  
  2. using System.Runtime.InteropServices;  
  3.   
  4. namespace TestCLR2Crash {  
  5.     [StructLayout( LayoutKind.Explicit )]  
  6.     struct Reinterpreter {  
  7.         [FieldOffset( 0 )]  
  8.         IntPtr _pointer;  
  9.         [FieldOffset( 0 )]  
  10.         object _target;  
  11.   
  12.         public IntPtr Cast( object obj ) {  
  13.             _target = obj;  
  14.             return _pointer;  
  15.         }  
  16.     }  
  17.   
  18.     static class Program {  
  19.         static void Main( string[ ] args ) {  
  20.             var reinterpreter = new Reinterpreter( );  
  21.             var arr = new[ ] { 1, 2, 3 };  
  22.             var ptr = reinterpreter.Cast( arr );  
  23.             Console.WriteLine( ptr.ToString( "X" ) );  
  24.         }  
  25.     }  
  26. }  

很可惜CLR已经预料到这种玩法,不允许值类型与引用类型的域交叠。上面的代码可以通过编译,但在加载Reinterpreter类型时会出错:
引用
Unhandled Exception: System.TypeLoadException: Could not load type 'TestCLR2Crash.Reinterpreter' from assembly 'TestCLR2Crash, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.
   at TestCLR2Crash.Program.Main(String[] args)

此路不通 =x=|||

---------------------------------------------------------------------------

其实.NET标准库里有System.Runtime.InteropServices.GCHandle这么个值类型。它的作用主要体现在托管代码与native code的互操作中,以避免需要用到的托管对象在native code执行过程中意外被GC回收。GCHandle也可以把对象“定”(pin)住,以避免native code访问对象的过程中对象地址发生改变。

GCHandle有个AddrOfPinnedObject()方法,正好可以提供对象的地址;使用GCHandle还附带了保证对象不被回收的功能,对这帖想要玩的代码是正合适。
创建GCHandle时用到的Alloc()方法要求调用它的程序集有SecurityPermissionFlag.UnmanagedCode权限。不过即便用了它,我也没有在代码中显式使用unsafe code,满足我的要求。

要用GCHandle定住一个对象的话,创建GCHandle时会对一些类型的对象给予特别待遇,包括:
1、System.String
2、元素为原始类型的数组
3、元素为值类型且Blittable的数组
4、其它Blittable类型
任何不在上述范围内的对象obj,传给GCHandle.Alloc(obj, GCHandleType.Pinned)的话,会引发异常:
C#代码
  1. using System;  
  2. using System.Runtime.InteropServices;  
  3.   
  4. namespace TestCLR2Crash {  
  5.     static class Program {  
  6.         static void Main( string[ ] args ) {  
  7.             var o = new object( );  
  8.             var handle = GCHandle.Alloc( o, GCHandleType.Pinned );  
  9.             var addr = handle.AddrOfPinnedObject( );  
  10.             handle.Free( );  
  11.         }  
  12.     }  
  13. }  

引用
Unhandled Exception: System.ArgumentException: Object contains non-primitive or
non-blittable data.
   at System.Runtime.InteropServices.GCHandle.InternalAlloc(Object value, GCHandleType type)
   at TestCLR2Crash.Program.Main(String[] args) in F:\document\Visual Studio 2008\Projects\TestDev9\TestCLR2Crash\Program.cs:line 8

换成一个字符串就没问题:
C#代码
  1. using System;  
  2. using System.Runtime.InteropServices;  
  3.   
  4. namespace TestCLR2Crash {  
  5.     static class Program {  
  6.         static void Main( string[ ] args ) {  
  7.             var str = "check me";  
  8.             var handle = GCHandle.Alloc( str, GCHandleType.Pinned );  
  9.             var addr = handle.AddrOfPinnedObject( );  
  10.             Console.WriteLine( "{0}, {1}", str, addr.ToInt32( ) );  
  11.             handle.Free( );  
  12.         }  
  13.     }  
  14. }  

GCHandle.Alloc()文档中对此有提及。我差点看漏了以为文档没说……

有趣的是,GCHandle对类型的挑剔还不止于此。GCHandle.AddrOfPinnedObject()方法乍一看像是返回对象自身的起始地址,实际不然,返回的是对象的“数据区”的起始地址。对System.String来说,“数据区”就是实际存放字符的char数组(CLR的String实现把char数组融合到String里了,没有单独的“char数组成员”。忽略ObjHeader,跳过MethodTable指针和其它成员,例如m_arrayLength、m_stringLength,跳到m_firstChar也就是融合后char数组的开始);而对数组来说,“数据区”就是实际存放值的区域(忽略ObjHeader,跳过MethodTable指针和Length);对其它Blittable类型来说,“数据区”就是Object里MethodTable指针之后的部分。

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

完事俱备,只欠开工写代码来实现前面讨论的内容。
先上代码:
C#代码
  1. using System;  
  2. using System.Reflection;  
  3. using System.Runtime.InteropServices;  
  4.   
  5. namespace TestCLR2Crash {  
  6.         static void Main( string[ ] args ) {  
  7.             // declare a delegate that refers to a static method,  
  8.             // in this case it's a static method generated from the  
  9.             // anonymous delegate.  
  10.             Action action = delegate( ) { };  
  11.   
  12.             // "generate" code into an array of uint  
  13.             var fakeDelegate = new uint[ ] {  
  14.                 // dummy values  
  15.                 0x00000000, 0x00000000,  
  16.                 // fake _methodPtrAux  
  17.                 0x00000000,  
  18.                 // native code/string  
  19.                 0x6AEC8B55, 0x2FD9B8F5, 0xD0FF7C81, 0x006A006A,  
  20.                 0x00E81F6A, 0x83000000, 0x50102404, 0x81CC5DBA,  
  21.                 0x8BD2FF7C, 0x47C35DE5, 0x74656572, 0x73676E69,  
  22.                 0x6F726620, 0x6567206D, 0x6172656E, 0x20646574,  
  23.                 0x65646F63, 0x00000A21  
  24.             };  
  25.   
  26.             // fill in the fake _methodPtrAux,  
  27.             // make it point to the code region in fakeDelegate  
  28.             var handle = GCHandle.Alloc( fakeDelegate, GCHandleType.Pinned );  
  29.             var addr = handle.AddrOfPinnedObject( );  
  30.             const int sizeOfUInt32 = sizeofuint ); // 4  
  31.             const int indexOfCode = 3;  
  32.             fakeDelegate[ 2 ] = Convert.ToUInt32( addr.ToInt32( ) + sizeOfUInt32 * indexOfCode );  
  33.   
  34.             var targetInfo = typeof( Action )  
  35.                 .GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );  
  36.             targetInfo.SetValue( action, fakeDelegate );  
  37.             action( );       // Greetings from generated code!  
  38.             Console.WriteLine( "Greetings from managed code!" );  
  39.   
  40.             handle.Free( );  
  41.         }  
  42.     }  
  43. }  

执行结果是:(再次提醒:在Vista或者Windows 7上运行会遇到AccessViolationException)
引用
Greetings from generated code!
Greetings from managed code!

Good!成功的让CLR执行了一段我们指定的native code,在标准输出流上显示了"Greetings from generated code!\n",而且没有显式使用P/Invoke或者unsafe code。为了演示从native code返回后CLR仍在正常运行,所以通过Console.WriteLine()再输出了一行"Greetings from managed code!"。
前文基本上已经把代码的原理解释得差不多了(吧?),所以这边就不再详细解释。
fakeDelegate数组里的native code/string那段可能不太直观,其实那就是前面给出的x86机器码以及"Greetings from generated code!\n"。需要注意的是因为x86的字节序(endian)是little-endian,低位字节在前;而本例中用的数组元素类型是uint,是4字节的整型,所以以4字节为单位,其中的顺序是“反”的。
就以第一个数字0x6AEC8B55为例,它在内存中是0x55 0x8B 0xEC 0x6A这4个字节,其中头3个字节就是这两条指令:
X86 asm代码
  1. 55              push ebp  
  2. 8BEC            mov  ebp,esp  

这样应该就好理解了吧?

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

回顾标题,“要让CLR挂掉的话”,上面的例子都还没让CLR挂掉,似乎有点不够意思。其实真要让CLR连最后的防护措施都挂掉、连异常都抓不到,那还挺难的。但我们可以很轻松的做出些例子,观察一下平时难得一见的异常。
在家实验的同学们千万注意了:要自行尝试引发错误的话,一定要小心,不要在有重要资料的系统上试。任意篡改代码或者栈上/堆上的数据,实际会引发什么后果很难预料。发生什么糟糕后果责任要自己承担的哦~
如果构造这样的一段代码:
X86 asm代码
  1. 83C4 08         add esp,8  
  2. C3              ret  

把它放到上面的C#例子里:
C#代码
  1. using System;  
  2. using System.Reflection;  
  3. using System.Runtime.InteropServices;  
  4.   
  5. namespace TestCLR2Crash {  
  6.         static void Main( string[ ] args ) {  
  7.             // declare a delegate that refers to a static method,  
  8.             // in this case it's a static method generated from the  
  9.             // anonymous delegate.  
  10.             Action action = delegate( ) { };  
  11.   
  12.             // "generate" code into an array of uint  
  13.             var fakeDelegate = new uint[ ] {  
  14.                 // dummy values  
  15.                 0x00000000, 0x00000000,  
  16.                 // fake _methodPtrAux  
  17.                 0x00000000,  
  18.                 // native code  
  19.                 0xC308C483  
  20.             };  
  21.   
  22.             // fill in the fake _methodPtrAux,  
  23.             // make it point to the code region in fakeDelegate  
  24.             var handle = GCHandle.Alloc( fakeDelegate, GCHandleType.Pinned );  
  25.             var addr = handle.AddrOfPinnedObject( );  
  26.             const int sizeOfUInt32 = sizeofuint ); // 4  
  27.             const int indexOfCode = 3;  
  28.             fakeDelegate[ 2 ] = Convert.ToUInt32( addr.ToInt32( ) + sizeOfUInt32 * indexOfCode );  
  29.   
  30.             var targetInfo = typeof( Action )  
  31.                 .GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );  
  32.             targetInfo.SetValue( action, fakeDelegate );  
  33.             action( );  
  34.             handle.Free( );  
  35.         }  
  36.     }  
  37. }  

执行,我们会看到什么呢?
引用
Process is terminated due to StackOverflowException.


把构造的代码改为
X86 asm代码
  1. 83C4 18         add esp,18h  
  2. C3              ret  

(也就是native code那段的数字是0xC318C483)
执行结果是:
引用
Unhandled Exception: System.Runtime.InteropServices.SEHException: External component has thrown an exception.

stack trace是:
引用
> 0012f4f9()
mscorwks.dll!_CallDescrWorker@20()  + 0x33 bytes
mscorwks.dll!_CallDescrWorkerWithHandler@24()  + 0x9f bytes
mscorwks.dll!MethodDesc::CallDescr()  + 0x15a bytes
mscorwks.dll!MethodDesc::CallTargetWorker()  + 0x1f bytes
mscorwks.dll!MethodDescCallSite::CallWithValueTypes()  + 0x1a bytes
mscorwks.dll!ClassLoader::RunMain()  - 0x39028 bytes
mscorwks.dll!Assembly::ExecuteMainMethod()  + 0xa4 bytes
mscorwks.dll!SystemDomain::ExecuteMainMethod()  + 0x416 bytes
mscorwks.dll!ExecuteEXE()  + 0x49 bytes
mscorwks.dll!__CorExeMain@0()  + 0x98 bytes
mscoree.dll!__CorExeMain@0()  + 0x34 bytes
kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

EIP停下的位置是:
X86 asm代码
  1. 0012F4F9 F4               hlt  

这个地址是Windows上很典型的栈地址。EIP居然跑到这个地方来,把数据当成指令执行了一句“HLT”指令……
HLT是一个Ring 0指令,用户模式的应用程序没办法是用这条指令,所以试图执行它引发了错误。
SEHException这种泛泛的异常少见吧?成功的搞出了一个没有映射到有具体含义的CLR异常,呵呵 ^ ^

P.S. 这帖告诉我们,类型安全的真面目就是:一旦你开始玩裸指针、玩union、玩goto,啥类型安全都是浮云……
P.P.S. 查了文档,Silverlight 3里的GCHandle.AllocGCHandle.AddrOfPinnedObject果然是SecurityCritical的,从用户代码里无法调用它。然而Type.GetMethodType.GetField之类还是Transparent的,还是有搞头——至少在Moonlight上 XD
P.P.P.S. 呜,但是FieldInfo.SetValue只能用来设置可访问的域的值……又没搞头了
MSDN 写道
In Silverlight, only accessible fields can be set using reflection.  

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:414811次
    • 积分:6866
    • 等级:
    • 排名:第3358名
    • 原创:198篇
    • 转载:16篇
    • 译文:21篇
    • 评论:180条
    博客专栏
    文章分类
    最新评论