zoukankan      html  css  js  c++  java
  • 挂掉.NET 2

    前一篇提到了通过改变委托中的指针来改变实际的调用目标。修改委托实例中的_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. }  
    using System;
    
    static class Program {
        static void Main(string[] args) {
            // block the program so that we could easily attach a debugger
            var name = Console.ReadLine();
            Console.WriteLine("Greet me, {0}", name);
        }
    }


    除了这个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. }  
    #include <windows.h>
    
    int main() {
        HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
        WriteConsole(hStdout, "Greetings from generated code!\n", 31, NULL, NULL);
        
        return 0;
    }


    很简单对吧?基本对应的机器码和汇编,后面还会用到:

    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  
    55              push ebp
    8BEC            mov  ebp,esp
    6A F5           push -0B                         ; /DevType = STD_OUTPUT_HANDLE
    B8 D92F817C     mov  eax,KERNEL32.GetStdHandle   ; |
    FFD0            call eax                         ; \GetStdHandle
    6A 00           push 0                           ; /pReserved = NULL
    6A 00           push 0                           ; |pWritten = NULL
    6A 1F           push 1F                          ; |CharsToWrite = 1F (31.)
    E8 00000000     call <&next_instruction>         ; |
    830424 10       add  dword ptr ss:[esp],10       ; |Buffer
    50              push eax                         ; |hConsole
    BA 5DCC817C     mov  edx,KERNEL32.WriteConsoleA  ; |
    FFD2            call edx                         ; \WriteConsoleA
    8BE5            mov  esp,ebp
    5D              pop  ebp
    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  
    mov         eax,ecx          ; 把第一参数(_target)复制到EAX
    mov         ecx,edx          ; 把原本的第二参数变为第一参数
    add         eax,10h          ; 把_target._methodPtrAux的地址设到EAX
    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. }  
    /*
     * Return the hash code for the specified object.
     */
    u4 dvmGetObjectHashCode(Object* obj)
    {
        return (u4) obj;
    }


    注意到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. }  
    uint32 address_hash(char* addr)
    {
      register uint32 key;
      key = (uint32) addr;
      return (key >> 3) * 2654435761;
    }



    那要是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. }  
    #include <stdio.h>
    
    typedef union tagMyUnion {
        int i;
        float f;
    } MyUnion;
    
    int main() {
        MyUnion u;
        int i;
        
        u.f = 2.0f;                                 // 0x40000000
        i = u.i;                                    // 0x40000000
        u.i = i + (1 << 23);                        // 0x40800000
        printf("%f\n", u.f);                        // 4.000000
        printf("%d, 0x%08x\n", i, i);               // 1073741824, 0x40000000
        printf("%d, 0x%08x\n", (int)u.f, (int)u.f); // 4, 0x00000004
        
        return 0;
    }


    在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. }  
    using System;
    using System.Runtime.InteropServices;
    
    namespace TestCLR2Crash {
        [StructLayout( LayoutKind.Explicit )]
        struct Reinterpreter {
            [FieldOffset( 0 )]
            IntPtr _pointer;
            [FieldOffset( 0 )]
            object _target;
    
            public IntPtr Cast( object obj ) {
                _target = obj;
                return _pointer;
            }
        }
    
        static class Program {
            static void Main( string[ ] args ) {
                var reinterpreter = new Reinterpreter( );
                var arr = new[ ] { 1, 2, 3 };
                var ptr = reinterpreter.Cast( arr );
                Console.WriteLine( ptr.ToString( "X" ) );
            }
        }
    }


    很可惜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. }  
    using System;
    using System.Runtime.InteropServices;
    
    namespace TestCLR2Crash {
        static class Program {
            static void Main( string[ ] args ) {
                var o = new object( );
                var handle = GCHandle.Alloc( o, GCHandleType.Pinned );
                var addr = handle.AddrOfPinnedObject( );
                handle.Free( );
            }
        }
    }
    引用
    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. }  
    using System;
    using System.Runtime.InteropServices;
    
    namespace TestCLR2Crash {
        static class Program {
            static void Main( string[ ] args ) {
                var str = "check me";
                var handle = GCHandle.Alloc( str, GCHandleType.Pinned );
                var addr = handle.AddrOfPinnedObject( );
                Console.WriteLine( "{0}, {1}", str, addr.ToInt32( ) );
                handle.Free( );
            }
        }
    }


    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. }  
    using System;
    using System.Reflection;
    using System.Runtime.InteropServices;
    
    namespace TestCLR2Crash {
            static void Main( string[ ] args ) {
                // declare a delegate that refers to a static method,
                // in this case it's a static method generated from the
                // anonymous delegate.
                Action action = delegate( ) { };
    
                // "generate" code into an array of uint
                var fakeDelegate = new uint[ ] {
                    // dummy values
                    0x00000000, 0x00000000,
                    // fake _methodPtrAux
                    0x00000000,
                    // native code/string
                    0x6AEC8B55, 0x2FD9B8F5, 0xD0FF7C81, 0x006A006A,
                    0x00E81F6A, 0x83000000, 0x50102404, 0x81CC5DBA,
                    0x8BD2FF7C, 0x47C35DE5, 0x74656572, 0x73676E69,
                    0x6F726620, 0x6567206D, 0x6172656E, 0x20646574,
                    0x65646F63, 0x00000A21
                };
    
                // fill in the fake _methodPtrAux,
                // make it point to the code region in fakeDelegate
                var handle = GCHandle.Alloc( fakeDelegate, GCHandleType.Pinned );
                var addr = handle.AddrOfPinnedObject( );
                const int sizeOfUInt32 = sizeof( uint ); // 4
                const int indexOfCode = 3;
                fakeDelegate[ 2 ] = Convert.ToUInt32( addr.ToInt32( ) + sizeOfUInt32 * indexOfCode );
    
                var targetInfo = typeof( Action )
                    .GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );
                targetInfo.SetValue( action, fakeDelegate );
                action( );       // Greetings from generated code!
                Console.WriteLine( "Greetings from managed code!" );
    
                handle.Free( );
            }
        }
    }


    执行结果是:(再次提醒:在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  
    55              push ebp
    8BEC            mov  ebp,esp


    这样应该就好理解了吧?

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

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

    X86 asm代码 复制代码
    1. 83C4 08         add esp,8  
    2. C3              ret  
    83C4 08         add esp,8
    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. }  
    using System;
    using System.Reflection;
    using System.Runtime.InteropServices;
    
    namespace TestCLR2Crash {
            static void Main( string[ ] args ) {
                // declare a delegate that refers to a static method,
                // in this case it's a static method generated from the
                // anonymous delegate.
                Action action = delegate( ) { };
    
                // "generate" code into an array of uint
                var fakeDelegate = new uint[ ] {
                    // dummy values
                    0x00000000, 0x00000000,
                    // fake _methodPtrAux
                    0x00000000,
                    // native code
                    0xC308C483
                };
    
                // fill in the fake _methodPtrAux,
                // make it point to the code region in fakeDelegate
                var handle = GCHandle.Alloc( fakeDelegate, GCHandleType.Pinned );
                var addr = handle.AddrOfPinnedObject( );
                const int sizeOfUInt32 = sizeof( uint ); // 4
                const int indexOfCode = 3;
                fakeDelegate[ 2 ] = Convert.ToUInt32( addr.ToInt32( ) + sizeOfUInt32 * indexOfCode );
    
                var targetInfo = typeof( Action )
                    .GetField( "_target", BindingFlags.NonPublic | BindingFlags.Instance );
                targetInfo.SetValue( action, fakeDelegate );
                action( );
                handle.Free( );
            }
        }
    }


    执行,我们会看到什么呢?

    引用
    Process is terminated due to StackOverflowException.



    把构造的代码改为

    X86 asm代码 复制代码
    1. 83C4 18         add esp,18h   
    2. C3              ret  
    83C4 18         add esp,18h
    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  
    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.
  • 相关阅读:
    Python 快速入门笔记(4):表达式
    Python 快速入门笔记(3):常量和变量
    selenium中的下拉框处理模块Select
    HTML基础之JS中的字符转义--转义中文或特殊字符
    HTML基础之JS中的序列化和反序列化-----字符串的json类型与字典之间的相互转换
    【转载】Jenkins安装以及邮件配置
    HTML基础
    python之用unittest实现接口参数化示例
    python之使用单元测试框架unittest执行自动化测试
    python之造测试数据-faker(转载)
  • 原文地址:https://www.cnblogs.com/qianyz/p/2407770.html
Copyright © 2011-2022 走看看