使用安全的C#代码跳出CLR沙箱

Our Team:http://www.ph4nt0m.org
Author:云舒(wustyunshu@hotmail.com)

本文可以任意转载,但请保持完整,并保留原始出处,谢谢。

起因

      一个老外写了一篇文章<<Writing a .NET Security Exploit POC>>,他给出的代码地址为http://www.frijters.net/TypeSafetyExploitPoC.cs.txt。KJ看到后觉得可以用在SQL Server 2005里面,通过加载C#代码绕过沙箱的限制来获取shell。他测试之后代码无法运行,我就进行了一些初步调试,发现那段代码在内存中暴力搜索WinExec函数的地址时失败,返回为0,因此无法顺利执行。

      这时候刺也加了进来,我们分头同时想到不需要在C#里面做内存搜索,既然可以写入机器码,那么直接写入一段shellcode就可以了。于是我在http://metasploit.com找了一段shellcode,修改原始POC,刺也同时作出了可以成功运行的修改版POC代码。

      晚上KJ拿回家在SQL Server 2005中测试,确实可以,但是限制条件还是比较多,所以基本上是鸡肋了,可以参看他的文章,地址为http://blog.csdn.net/kj021320/archive/2008/09/17/2944371.aspx。但是事情没有完,这个代码不复杂,但是原理是什么?他们都不太熟悉C#,因此责任就落在我身上了,开始了痛苦的虚拟机中运行的代码的调试之旅。至于这段POC代码,我发到我们的邮件列表了,地址是http://groups.google.com/group/ph4nt0m/browse_thread/thread/1ee957d07b33931f/6a0e158cb1deb078?show_docid=6a0e158cb1deb078,在我的回复里面,这里就不写出来了。

原理

      要了解这个POC的原理,首先需要知道一些CLR的基础。这些没找到文档资料,是我跟踪分析出来的,不一定对,姑妄听之吧。在C#中直接定义数组,是在栈中保存元素的。但是如果数组是一个类中的成员,那么实例化这个类并且初始化这个数组的时候,在栈中仅仅保存一个指针,这个指针指向真正存放元素的地方。被指向的内存,第一个四字节保存着MethodTable,第二个四字节保存着元素的个数,第三个四字节才开始是保存着真实的数组元素。

      在POC里,类Union2中有一个arr数组作为其成员,另一个o成员在后面被一个委托赋值,其实就是一个函数指针,4字节。而拥有u1和u2实例的UnsafeUnion结构体,申明了[StructLayout(LayoutKind.Explicit)]和[FieldOffset(0)],表示u1和u2在内存中的偏移都是0字节。由于u1是两个int,均是4字节,而u2的o成员和arr都是指针,也都是4字节。因此,u1的i元素和u2的o元素在内存中重合,u1的j元素和u2的arr元素在内存中重合,u1和u2本身在内存中也完全融合。

      POC中的del委托指向DummyMethod函数,在CLR语言中无法通过安全的代码来获取函数指针,因此只能通过将del赋值给object o来间接的传递函数地址。最终通过修改int型j的值,间接的修改了arr指向的地址,将arr指向了DummyMethod函数中。最终导致修改arr,其实是修改了函数DummyMethod的代码,通过委托执行函数的时候就执行了我们自定义的shellcode,跳出了sandbox了。

      那个POC的作者熟悉C#而不太熟悉安全,所以代码长而且不够可靠,修改了下就好了。原理说得有些绕,下面我来用动态跟踪证实这一点。

调试

      C#代码是在CLR虚拟机里面运行的,因此基本无法用OllyDBG来做调试,如果有人有办法,还请指点一下。经过在微软一番搜索,使用了Visual Studio 2008加载SOS.dll来做动态跟踪,具体的文档可以搜索SOS.dll就行了。下面简单介绍一下我的调试过程,因为觉得这个很有意思。当然这个是摸索过后的情形,所以看起来很顺利,其实我从学习SOS.dll到跟踪分析完成花了两天多时间。

      首先在加载我修改过的代码工程,在Main函数的第一行下断点,并打开Memory窗口,再在右下角的Immediate Window窗口中输入命令.load sos加载sos.dll模块,整个界面如下图所示,希望这是唯一的一个截图。

      .load sos
      extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

      可以看到,成功的加载了sos.dll,可以使用!help命令查看sos模块提供的调试命令帮助信息,更多的请自己翻阅手册。
单步走一步,查看CLR虚拟机的栈信息,结果如下:
!clrstack -a
OS Thread Id: 0x8f4 (2292)
ESP       EIP    
0012f418 00e500e4 TypeSafetyExploitPoC.Main(System.String[])
    PARAMETERS:
        args = 0x01301628
    LOCALS:
        0x0012f440 = 0x00000000
        0x0012f43c = 0x01301664
        0x0012f438 = 0x00000000
        0x0012f434 = 0x00000000
        0x0012f430 = 0x00000000
        0x0012f42c = 0x00000000
        0x0012f428 = 0x00000000
        0x0012f424 = 0x00000000
        0x0012f420 = 0x00000000
        0x0012f41c = 0x00000000
        0x0012f418 = 0x00000000
0012f69c 79e7c74b [GCFrame: 0012f69c]

      可以看到,变量u2已经被定义了,在栈里面的地址为0x01301664。再单步走到u2.o = del上面的语句,查看一下栈,会发现u1,u2,以及del都定义了,而且u1和u2指向的内存和我上面说的一样,是重合的,都是0x01301664——当然,具体的内存地址取决于机器。Del的值为0x01301674。

      在Memory窗口,查看0x01301664地址处的内存,单步走完u2.o = del这条语句,再查看内存,会发现0x01301664 + 4处的内存变成了del的值,即0x01301674,需要注意的是,u1和u2在内存上完全重合,因此u1.i的值就也变成了0x01301674。为什么是0x01301664 + 4处的内存而不是0x01301664处?查看一下u2的结构,如下:

!dumpobj 0x01301664
Name: Union2
MethodTable: 009830fc
EEClass: 00981344
Size: 16(0x10) bytes
 (E:\TestExp\bin\Debug\TestExp.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790fd0f0  4000003        4        System.Object  0 instance 01301674 o
7912d7c0  4000004        8      System.Int32[]  0 instance 00000000 arr

      可以看到,u2的第一个元素偏移量确实是4,偏移为0的地方,是存放着MethodTable的地址,记录了一些内部信息。
单步走过u1.j = u1.i,可以看到内存0x01301664 + 8处的值改变了。还是由于u1和u2内存重合的原因,u1.i,u1.j,以及u2.o,u2.arr都变成了相同的值,即del的值,也就是那个函数指针的值了。

      下面的一条指令u1.j = u2.arr[2] – 12是POC最关键的一条,还是由于内存重合,这一条改变u1.j的值的指令,间接的修改了arr指向的地址,使arr指向了函数指针指向的内存。因此在后面对arr进行修改的时候,其实是修改了函数指针指向的内存,也就是修改了委托del指向的函数DummyMethod的指令。这条代码执行后查看arr的值,如下:
!dumpobj 0x01301664
<Note: this object has an invalid CLASS field>
Name: Union2
MethodTable: 009830fc
EEClass: 00981344
Size: 16(0x10) bytes
 (E:\TestExp\bin\Debug\TestExp.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790fd0f0  4000003        4        System.Object  0 instance 01301674 o
7912d7c0  4000004        8      System.Int32[]  0 instance 0098c054 arr

      可以看到,数组的数据已经是保存在地址0x0098c054处了。我们看一下函数指针0x01301674的内容。

!dumpobj 0x01301674
Name: System.Threading.ThreadStart
MethodTable: 791249e8
EEClass: 790c57b8
Size: 32(0x20) bytes
 (C:\WINDOWS\assembly\GAC_32\mscorlib.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790fd0f0  40000ff        4        System.Object  0 instance 01301674 _target
7910ebc8  4000100        8 …ection.MethodBase  0 instance 0130182c _methodBase
791016bc  4000101        c        System.IntPtr  1 instance 003B20C4 _methodPtr
791016bc  4000102       10        System.IntPtr  1 instance 0098C060 _methodPtrAux
790fd0f0  400010c       14        System.Object  0 instance 00000000 _invocationList
791016bc  400010d       18        System.IntPtr  1 instance 00000000 _invocationCount

      注意其中的_methodPtrAux的值,对比一下上文的arr指向的内存,发现确实落在其中了。其实上面的arr[2]取到的值就是这个_methodPtrAux的内容。后面的事情就简单了,将shellcode拷贝到arr中,也就是拷贝到了DummyMethod函数的内存中,改写了DummyMethod函数。最后调用del委托间接的调用DummyMethod,就执行了我们的shellcode,突破sandbox了。

相关日志

发表评论