-
-
[原创]CLR v4 HOOK JIT 的一些变化
-
发表于: 2012-8-6 11:58 15343
-
新人发帖,高手们就不要看了,过程很简单,仅作抛砖引玉。
网上有很多JIT Injection的文章,但都是针对.net framework runtime 2.0的。我也用这样的JIT Injection做过一些自己的小程序,比如Display Calls,以便在破解.net assembly的过程中,找到调用顺序。但是到了CLR 4.0,Display Calls不起作用了。于是经过研究,终于调试好了Display Calls的程序。下面是整个的研究过程:
1. 在网上搜索 .net 4 compileMethod,可以发现getJit这个方法现在在clrjit.dll中,而不是在原来的mscorjit.dll中。于是修改GetModuleHandle处代码为:
hJitMod = GetModuleHandle(“clrjit.dll”);
2. ICorJitInfo类的变化。
如果不想得到method的name,上面的修改足以让Display Calls程序跑起来,但我们的目的是显示即时编译的方法和所在的类名,因此必须调整这个类。
为什么调整这个类呢?首先让我们看看在runtime 2.0下,调用我们自己的compileMethod时,这个类是怎样的一个内存分布。首先用windbg载入Display Calls的程序,在my_compileMethod上设置断点,当程序中断后,查看参数:
根据x64程序的调用约定,第一个参数放到rcx(xmm0)中,第二个参数放到rdx(xmm1)中,第三个参数放到r8(xmm2)中,第四个参数放到r9(xmm3)中,剩下的参数从左向右依次放到堆栈上。用dv命令也可以查看参数,但是不知道为什么和寄存器中的不一样,且dv显示的地址也是不正确的。
而我们是要调用ICorJitInfo接口中的getMethodName,ICorJitInfo是第二个参数,因此它的地址在rdx中,调用dps @rdx可以显示ICorJitInfo实现类的vftable地址:
我们可以发现具体实现类是CEEJitInfo,虚函数表地址是7fef9537d60,接着继续dps 7fef9537d60:
虚表中的方法是顺序排列的,getMethodName相对虚表基址的偏移量是7fef9537de0-7fef9537d60=0x80。
接下来我们看看runtime 4.0下的情况,滤过一些过程,直接看看传入my_compileMethod的第二个参数及它的虚表:
实际虚表地址是7fef8f890c0。再次使用dps命令:
0:000> dps 7fef8f890c0 l 100
000007fe`f8f890c0 000007fe`f88fe4d0 clr!CEEInfo::getMethodAttribs
000007fe`f8f890c8 000007fe`f89017b0 clr!CEEInfo::setMethodAttribs
000007fe`f8f890d0 000007fe`f88fad30 clr!CEEInfo::getMethodSig
000007fe`f8f890d8 000007fe`f88fe9d0 clr!CEEInfo::getMethodInfo
000007fe`f8f890e0 000007fe`f88fb490 clr!CEEInfo::canInline
000007fe`f8f890e8 000007fe`f88fc470 clr!CEEInfo::reportInliningDecision
......
000007fe`f8f893e0 000007fe`f8908a4c clr!CEEInfo::getMethodName
......
或者使用 x命令()
0:000> x clr!ceeinfo::getMethodName
000007fe`f8908a4c clr!CEEInfo::getMethodName = <no type information>
我们会发现虚表已经发生了很大的变化,现在的偏移量是0x320,这说明内部的CEEJitInfo和CEEInfo已经发生了变化。
但是编译器在生成pe文件时,不会动态根据两个ceeinfo之间的不同而生成不同的代码,因此静态编译时,产生的是相同的代码,即都是根据头文件中定义的ICorJitInfo来产生的代码,因此通过虚表地址来找到getMethodName的地址的代码也是相同的。
另一方面,我们的my_compileMethod的第二个参数只是个地址,我们现在定义成ICorJitInfo只是一个符号,真正的可能也不是这个名字,这个地址是运行时根据实际的ceeinfo生成的对象的地址,无论我们的接口定义成ICorJitInfo还是其它的,实际都指向CEEInfo这个对象的虚表地址,但是由于两个runtime下CEEInfo虚表结构的变化,静态生成的一样的代码来读取动态的数据结构发生了变化的内容,其结果可想而知。因此我们要更改ICorJitInfo这个接口的定义,使其由定义产生的虚表和实际的虚表吻合,这样编译出来的程序就能读取实际的虚表了。
3. 获得真正的虚表
其实上面阐述的过程已经涉及到了,就是利用dps [vftable address] l [长度]。但是长度是多少不好定义,我也不知道怎么能获得虚表的确切长度。好在我们还可以利用上面的x命令,比如:x clr!ceeinfo::*,这样就可以知道最后一个函数是什么。
4. 使用新的类,比如ICorJitInfoV4
之后为了编译器能生成匹配真实虚表的代码,我们需要在新的类中填写对应的虚函数。但通过WinDbg只能获得函数名,如何获得函数的参数及返回值呢?这里可以利用PDBExplorer这个工具加载pdb文件来获得,前提是你有pdb文件。这个容易,用WinDbg连接微软的符号服务器,可以下载到对应的pdb文件,之后用PDBExplorer加载pdb,找到getMethodName方法,如图:
这里需要按顺序将真实虚表中的函数添加到新的类中,记住一定要按顺序,且应该都是虚函数。其实知道了getMethodName在真实虚表中的偏移后,比如0x320,x64地址是8为,因此getMethodName是虚表中第100个函数,那么在之前插入99个没用的虚函数即可,比如virtual void __stdcall Func1~99,这样不会有问题,因为我们的程序中只用到了getMethodName方法,其他方法我们并没有用到,也就是说在运行的过程中也不会用到。至于其他的pe文件即使用到了,也一定是在各自pe文件中定义了这些函数的具体名字。
另外,不要像原来的ICorJitInfo类那样继承于ICorDynamicInfo,这样会导致编译器还会按照继承关系来生成对应的代码,而不是用我们填写的这些函数。去掉继承关系即可编译生成正确的代码
至此,Display Calls又可以正常工作啦 ^_^