首页
社区
课程
招聘
[原创]从逆向的角度看.NET的几个基本概念(续)――.NET中的Method
发表于: 2006-9-26 10:50 12451

[原创]从逆向的角度看.NET的几个基本概念(续)――.NET中的Method

2006-9-26 10:50
12451

从逆向的角度看.NET的几个基本概念(续)――.NET中的Method
    上篇讲了很多概念,但没有针对任何一个进行深入。这次我们来讲讲最有趣的Method,它在.NET中的作用相当于win32下的function。Method之所以有趣正是因为它和JIT引擎及其它.NET内核的关系最为密切:JIT得到Method的IL代码并把它编译成本机代码,如果在编译过程中遇到调用新的类,JIT可能转到新类的编译中,内核要根据Method定义的属性决定代码是否能够调用一个Method(当然,这个工作大多数在静态编译阶段完成),.NET中的inline是怎样实现的。最关键的是,我们要寻找的敏感代码,肯定隐藏在某个(或数个)Method中。
    我们会从一个简单例子着手,通过动态调试的方法走进.NET的内核。例子代码如下:
using System;

namespace tankaiha.dotnetsample.sample1
{
    public sealed class calcclass
    {
        public void calcCode(string strCode)
        {
            if(strCode=="tankaiha")
            {
                Console.WriteLine("You got it");
            }
            else
            {
                Console.WriteLine("You are wrong!");
            }
            return;
        }
    }

    class mainclass
    {
        static void Main()
        {
            Console.WriteLine("Please enter you code:");
            calcclass cs=new calcclass();
            cs.calcCode(Console.ReadLine());
            return;
        }
    }
}

    代码有两个类,mainclass类中只包含了Main()这个入口方法,并从命令行读入用户的输入。另一个类calcclass中只有一个方法calcCode(),用来计算用户的输入是否等于tankaiha,然后分别输出信息。
    先来看看PE文件的Metadata,特别是calcCode方法。.NET PE头中,紧接着IMAGE_COR20_HEADER的就是Method Stream,程序中的方法及其IL代码全部在这里。共有四个方法,除了我们写的main和calcCode外,还有系统自已加上的默认.ctor初始化方法。

    这里并没有定义Method的属性,只是Method Body和Method Head。还记得过去讲过的Method Head分Tiny和Fat,这里显示calcCode()方法为IMAGE_COR_ILMETHOD_FAT,说明它是个Fat方法。
    对于Method的定义,则是存储在Metadata的MethodDef表中。如下:


   

    这里有两项很有意思,一是ImplFlags,另一个是Flags。
    Flags分为三大类,一类是控制放问的Access,比如你在程序中定义的public、private等。图中的ReuseSlot是牵涉到类在继承时方法的调用属性。第二类是控制InterOp的,第三类是额外属性。
    ImplFlags则分为两大类,一类是InterOp相关的,在我们的例子中不存在。另一类描述代码属性的,比如:
IL        0x0000        Method impl is CIL.
Native        0x0001        Method impl is native.
Managed        0x0000        Method impl is managed.
NoInlining        0x0008        Method may not be inlined.
NoInlining        0x0008        Method may not be inlined.
    从图中看出我们代码属性很简单,就是IL托管代码。刚才表中最后一项NoInling说明该方法不可以被内联,本例中并没有这个属性。后面会看到它的作用。

    看一下Main方法的IL代码,看一下C#编译到IL的大体情况。Main方法的代码如下:
.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       33 (0x21)
  .maxstack  2
  .locals init (class tankaiha.dotnetsample.sample1.calcclass V_0)
  IL_0000:  nop
  IL_0001:  ldstr      "Please enter you code:"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  newobj     instance void tankaiha.dotnetsample.sample1.calcclass::.ctor()
  IL_0011:  stloc.0
  IL_0012:  ldloc.0
  IL_0013:  call       string [mscorlib]System.Console::ReadLine()
  IL_0018:  callvirt   instance void tankaiha.dotnetsample.sample1.calcclass::calcCode(string)
  IL_001d:  nop
  IL_001e:  br.s       IL_0020
  IL_0020:  ret
} // end of method mainclass::Main

     代码流程非常清楚,其中编译器为我们加上了一个局部变量用来保存对calcclass实例化的值,通过stloc.0和ldloc.0。
.locals init (class tankaiha.dotnetsample.sample1.calcclass V_0)
而且代码中存在两种调用方式call和callvirt。在本地代码中我们会看到它实现方法的不同。

     下面开始动态调试,这里我们用OllyDbg,这样可以直接跟踪JIT生成的本机代码。由于OD不能直接中断在.NET程序的入口处,所以在我们第一次运行时是无法中断的。这没事,我们可以在IL代码被第一次读取时中断。因此,先设置程序在载入新的模块时中断,当mscorwks被载入后,我们中断在它的79E9776D处。


此处的代码如下
.text:79E9776D  call    dword ptr [ecx] ; call mscorjit.dll compMethods
这是调用mscorjit.dll并对Method进行编译。为什么?这是我跟出来的。当然,这个地址在各个机器上可能不同,这没事,你只要在mscorwks的反汇编中找到这个函数就可以了
enum  CorJitResult __stdcall invokeCompileMethodHelper(……)
这时再看模块,mscorjit.dll已经载入了。(不知为什么,OD没法在它加载时中断,只好用这个麻烦一点的方法了)

    mscorjit.dll载入后,便可以中断在它的private: virtual enum  CorJitResult __stdcall CILJit::compileMethod处,在我机器上的地址是:bp 7906e7f4。
    F9运行后,程序中断。在xenocode的文章中我讲过了这时椎栈里的参数,我们要根据这个参数来看看是哪个方法被JIT了。
第一次的椎栈如下:

    其中被编译的IL代码的偏移在00402094处,这是mainclass::main方法。(为什么?自已想想)我们就是要看它的本地代码。F8步进,直到程序执行完一个call。
7906E82C  |.  E8 10000000     call mscorjit.7906E841
7906E831  |.  85C0            test eax,eax
7906E833  |.  75 08           jnz short mscorjit.7906E83D
7906E835  |.  8B4D 18         mov ecx,dword ptr ss:[ebp+18]
7906E838  |.  8B55 FC         mov edx,dword ptr ss:[ebp-4]
7906E83B  |.  8911            mov dword ptr ds:[ecx],edx
7906E83D  |>  C9              leave
7906E83E  \.  C2 1800         retn 18

此时,edx中存储的就是本地代码,在00DA0070处。我们中断:bp edx,再次F9运行。这时程序果然中断在00DA0070处了。完整代码如下:
00DA0070    56                push esi
00DA0071    833D 84103B02 00  cmp dword ptr ds:[23B1084],0
00DA0078    75 0A             jnz short 00DA0084
00DA007A    B9 01000000       mov ecx,1
00DA007F    E8 88D75A78       call mscorlib.7934D80C
00DA0084    8B0D 84103B02     mov ecx,dword ptr ds:[23B1084]
00DA008A    8B15 3C303B02     mov edx,dword ptr ds:[23B303C]
00DA0090    8B01              mov eax,dword ptr ds:[ecx]
00DA0092    FF90 D8000000     call dword ptr ds:[eax+D8]
00DA0098    B9 8030A700       mov ecx,0A73080
00DA009D    E8 7A1FCCFF       call 00A6201C
00DA00A2    8BF0              mov esi,eax
00DA00A4    E8 0F6B6178       call mscorlib.793B6BB8
00DA00A9    8BC8              mov ecx,eax
00DA00AB    8B01              mov eax,dword ptr ds:[ecx]
00DA00AD    FF50 64           call dword ptr ds:[eax+64]
00DA00B0    8B15 40303B02     mov edx,dword ptr ds:[23B3040]
00DA00B6    8BC8              mov ecx,eax
00DA00B8    E8 33B85A78       call mscorlib.7934B8F0
00DA00BD    25 FF000000       and eax,0FF
00DA00C2    0F94C0            sete al
00DA00C5    0FB6C0            movzx eax,al
00DA00C8    85C0              test eax,eax
00DA00CA    75 27             jnz short 00DA00F3
00DA00CC    833D 84103B02 00  cmp dword ptr ds:[23B1084],0
00DA00D3    75 0A             jnz short 00DA00DF
00DA00D5    B9 01000000       mov ecx,1
00DA00DA    E8 2DD75A78       call mscorlib.7934D80C
00DA00DF    8B0D 84103B02     mov ecx,dword ptr ds:[23B1084]
00DA00E5    8B15 44303B02     mov edx,dword ptr ds:[23B3044]
00DA00EB    8B01              mov eax,dword ptr ds:[ecx]
00DA00ED    FF90 D8000000     call dword ptr ds:[eax+D8]
00DA00F3    5E                pop esi
00DA00F4    C3                retn
    当程序执行到00DA0092时,我们看edx指向什么?
013B1A04  E0 A3 0F 79 17 00 00 00 16 00 00 00 50 00 6C 00  啵 y ... ...P.l.
013B1A14  65 00 61 00 73 00 65 00 20 00 65 00 6E 00 74 00  e.a.s.e. .e.n.t.
013B1A24  65 00 72 00 20 00 79 00 6F 00 75 00 20 00 63 00  e.r. .y.o.u. .c.
013B1A34  6F 00 64 00 65 00 3A 00 00 00 00 00              o.d.e.:.....
    这正是我们在Main方法中输入的”Please enter your code”的Unicode形式。看来这个call就是调用mscorlib.WriteLine方法。再往下走,怪了,在00DA00AD处调用ReadLine后,程序没有像我们设计的那样,跳转到calcclass的calcCode方法中,而是直接在00DA00B8处进行比较了。为什么呢?还记得前面提到的inline的问题吗?calcCode函数被自动内联了。
    来看一下C#里对inline的描述(摘自Professional C#):A method or property whose implementation simply calls another method or returns a field will almost certainly be inlined. 也就是说CLR在动态编译时自己决定哪些函数应该被内联,而没有inline类似的关键词来定义。
下面再做个试验,手动将calcCode的ImpleFlag加上NoInline。将0x3C2处的0改为8,



    再次打开sample.exe,发现头文件已经改变了,calcCode已有了NoInline的属性。


   
    下面同样用OD进行调试,看JIT后的代码发生了哪些改变。按前文所说的方法中断下来以后,这次本地代码的地址仍然是00DA0070。中断后,我们来到Main方法的本地代码处:
00DA0070    56                push esi
00DA0071    833D 84103B02 00  cmp dword ptr ds:[23B1084],0
00DA0078    75 0A             jnz short 00DA0084
00DA007A    B9 01000000       mov ecx,1
00DA007F    E8 88D75A78       call mscorlib.7934D80C
00DA0084    8B0D 84103B02     mov ecx,dword ptr ds:[23B1084]
00DA008A    8B15 3C303B02     mov edx,dword ptr ds:[23B303C]
00DA0090    8B01              mov eax,dword ptr ds:[ecx]
00DA0092    FF90 D8000000     call dword ptr ds:[eax+D8]
00DA0098    B9 8030A700       mov ecx,0A73080
00DA009D    E8 7A1FCCFF       call 00A6201C
00DA00A2    8BF0              mov esi,eax
00DA00A4    E8 0F6B6178       call mscorlib.793B6BB8
00DA00A9    8BC8              mov ecx,eax
00DA00AB    8B01              mov eax,dword ptr ds:[ecx]
00DA00AD    FF50 64           call dword ptr ds:[eax+64]
00DA00B0    8BD0              mov edx,eax
00DA00B2    8BCE              mov ecx,esi
00DA00B4    3909              cmp dword ptr ds:[ecx],ecx
00DA00B6    FF15 B830A700     call dword ptr ds:[A730B8]

00DA00BC    5E                pop esi
00DA00BD    C3                retn
    光从数量上看,这次的代码只有21行,比上次少了十来行。我们的重点放在粗体的最后四行上面。
mov edx,eax:把输入的字符串地址传给edx
mov ecx,esi:还记得我们的MSIL代码中调用calcCode()方法是callvirt吗?对于virt方法的调用,CLR要求将调用对象的Refrence传给ecx。这里也看出,JIT产生的代码是__fastcall形式,如果参数较多,可能edx,ecx,甚至eax都会被用来传递参数。
call dword ptr ds:[A730B8]:调用calcCode()方法。
    看一下第二句,mov ecx,esi,也就是说上面有一句是将calcClass初始化后,将它的refrence一直保存在esi中。就是这三句:
00DA0098    B9 8030A700       mov ecx,0A73080
00DA009D    E8 7A1FCCFF       call 00A6201C
00DA00A2    8BF0              mov esi,eax
    很明显,calcClass的refrence保存在00A73080处,而相对这个地址的0x38处就是calcCode()方法的地址(00A730B8-00A73080=0x38)。那么下面是最有趣的部分,我们看一下在系统对calcClass进行初始化前和初始化后,refrence中的内容都有哪些改变。实际上,00A73080处为一个CORINFO_CLASS_STRUCT结构,只是这个结构的具体参数微软未公开。在IA-32的机器上,这个结构有个40字节长的头部,紧跟着是各个方法的地址。(类的继承中,方法的各种属性就体现在这个地址表VTable中,这里不多说,有兴趣的自已查看相关文献。)
    初始化前,内容为:
00A73080  00 00 04 00 0C 00 00 00 02 04 06 00 04 00 00 00  .. .....   . ...
00A73090  18 9C 0F 79 14 2C A7 00 C0 30 A7 00 58 13 A7 00   ?y ,???X ?
00A730A0  00 00 00 00 00 00 00 00 EC 4B 35 79 C0 39 35 79  ........焖5y?5y
00A730B0  B0 39 35 79 C0 A4 34 79 C8 30 A7 00 D4 30 A7 00  ?5y坤4y????
00A730C0  80 00 00 00 00 00 00 00 B8 70 30 A7 00 89 ED E9  ?......葛0??
00A730D0  38 EE 91 FF B8 78 30 A7 00 89 ED E9 2C EE 91 FF  8??x0?????
00A730E0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    红体的部分就是[00A730B8],也就是下面将调用的calcCode的地址,这时的值是00A730C8,有意思,这个地址就指向本结构内。
00A730C8    B8 70 30 A7 00 89 ED E9
    这其实是一段反汇编代码,我们看一下它的asm指令。
00A730C8    B8 7030A700     mov eax,0A73070
00A730CD    89ED            mov ebp,ebp
00A730CF  - E9 38EE91FF     jmp 00391F0C

00A730D4    B8 7830A700     mov eax,0A73078
00A730D9    89ED            mov ebp,ebp
00A730DB  - E9 2CEE91FF     jmp 00391F0C
    就是一个跳转。跳转到00391F0C处的内容是:
00391F0C    50              push eax
00391F0D    52              push edx
00391F0E    68 A01BE779     push 79E71BA0
00391F13    55              push ebp
00391F14    53              push ebx
00391F15    56              push esi
00391F16    57              push edi
00391F17    8D7424 10       lea esi,dword ptr ss:[esp+10]
00391F1B    FF76 0C         push dword ptr ds:[esi+C]
00391F1E    55              push ebp
00391F1F    89E5            mov ebp,esp
00391F21    51              push ecx
00391F22    52              push edx
00391F23    64:8B1D 380E000>mov ebx,dword ptr fs:[E38]
00391F2A    8B7B 0C         mov edi,dword ptr ds:[ebx+C]
00391F2D    897E 04         mov dword ptr ds:[esi+4],edi
00391F30    8973 0C         mov dword ptr ds:[ebx+C],esi
00391F33    68 7CC52A05     push 52AC57C
00391F38    56              push esi
00391F39    E8 8C9BAE79     call mscorwks.79E7BACA
00391F3E    897B 0C         mov dword ptr ds:[ebx+C],edi
00391F41    8B4E 08         mov ecx,dword ptr ds:[esi+8]
00391F44    8946 08         mov dword ptr ds:[esi+8],eax
00391F47    8BC1            mov eax,ecx
00391F49    83C4 04         add esp,4
00391F4C    5A              pop edx
00391F4D    59              pop ecx
00391F4E    89EC            mov esp,ebp
00391F50    5D              pop ebp
00391F51    83C4 04         add esp,4
00391F54    5F              pop edi
00391F55    5E              pop esi
00391F56    5B              pop ebx
00391F57    5D              pop ebp
00391F58    83C4 08         add esp,8
00391F5B    C3              retn
    好,下面我们让代码对calcClass进行初始化,再看refrence处的内容(执行到最后的retn处停住):
00A73080  00 00 04 00 0C 00 00 00 02 04 06 00 04 00 00 00  .. .....   . ...
00A73090  18 9C 0F 79 14 2C A7 00 C0 30 A7 00 58 13 A7 00   ?y ,???X ?
00A730A0  00 00 00 00 00 00 00 00 EC 4B 35 79 C0 39 35 79  ........焖5y?5y
00A730B0  B0 39 35 79 C0 A4 34 79 D0 00 DA 00 D4 30 A7 00  ?5y坤4y????
00A730C0  80 00 00 00 00 00 00 00 B8 70 30 A7 00 89 ED E9  ?......葛0??
00A730D0  38 EE 91 FF B8 78 30 A7 00 89 ED E9 2C EE 91 FF  8??x0?????
00A730E0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  

    注意,00A730B8处的内容已经改变,这正是calcCode的本地代码地址。
    把整个过程小结一下。在第一次调用还没有进行JIT的方法时,VTable的地址总是指向一个prework,它会调用mscorwks,继而是JIT引擎,对该方法进行编译。编译完后的地址代码地址被直接写入方法表中,这样下次再调用该方法就是直接跳转到该地址,而不用再次进行编译。CLR中的方法调用大多数是__fastcall形式,利用寄存器来传递参数。而内联,在.NET中由JIT引擎在动态编译时自行决定的。
    本文利用OllyDbg对.NET下方法的调用和JIT进行一些跟踪,其中提到的一些中断的位置和技巧对Crack Fans们是很有用的,而了解.NET内部的一些运行机制,也会对我们的逆向过程有所帮助。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 7
支持
分享
最新回复 (6)
雪    币: 260
活跃值: (81)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
让我抢到了沙发
2006-9-26 11:25
0
雪    币: 89
活跃值: (171)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
呵呵,,,好文章不跟说不过去 
2006-9-26 11:39
0
雪    币: 263
活跃值: (10)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
4
这种贴要顶,对我来说还是看不懂
2006-9-26 11:41
0
雪    币: 1316
活跃值: (512)
能力值: ( LV12,RANK:450 )
在线值:
发帖
回帖
粉丝
5
顶!好文!
2006-9-26 11:58
0
雪    币: 5275
活跃值: (456)
能力值: (RANK:1170 )
在线值:
发帖
回帖
粉丝
6
看过dreaman的几篇,对.net也很有了解。有兴趣讨论和我联系
可以把QQ号发到:dotnetreverseteam[at]126[dot]com,回头我加你
2006-9-26 16:58
0
雪    币: 563
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
收下了回去看
2010-10-19 10:51
0
游客
登录 | 注册 方可回帖
返回
//