从逆向的角度看.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直播授课
上传的附件: