EasyHook的使用

在函数调用下钩子的做法,在制作非想天则对战平台以及Vorbis版的东方弹幕游戏,以及数据文件压缩版的东方非想天则的时候都做过了。无非也就是两种做法,一个是修改IAT,编译器在生成调用DLL里函数的代码的时候,用的是JMP DWORD [mm32]这样的语句,Windows在把EXE读入内存以后,查找它引用的DLL里的函数,然后加载这些DLL,然后把函数的地址填入一个表(数组)中,mm32就是这样的函数地址被填入的地方。那么只要修改这个地址,就能修改调用DLL中的函数的时候真正调用的函数。另一做法是修改函数入口处的机器代码,改为一个JMP rel32这样的语句,使得调用指定函数的时候会直接JUMP到自己的函数里面。这两种做法都要注意自己要调用真实函数的时候不要受影响就好了:导入表是以模块区分的,一个进程中的各个模块的导入表互不影响,所以如果是一个DLL把EXE的导入表改了,那么只要这个DLL的导入表是正常的,那么这个DLL中就可以随意调用原函数不受影响;修改入口处机器代码的做法,因为只修改了一个入口,所以能够执行入口处的几条指令,然后跳转到没有修改的地方继续执行下去,问题就能解决了。

 

原理虽然说都是这样,但是到了真正自己写的时候,一个是略麻烦,一个是也不一定一下子能写出稳定的代码。这个时候找现成的用是一种办法。上网搜了一圈以后发现EasyHook似乎不错。不过它是LGPL协议的,也就是说不想开源的话就得拖着个DLL,静态链接就必须把自己写的东西一起开源。看人家活了这么长时间了,而且连驱动程序下也能使用,应该比较稳定吧。它用的是Inline Hooking方式,就是前面提到的修改函数入口的做法。

然后这里我想说的主要还是关于这个库怎么用。我用到的它的有两套函数,Rh系列和Lh系列的。Rh是Remote Hooking,然后Lh是Local Hooking。Local指的是当前进程,就是自己给自己进程里的函数下钩子;Remote指的是其他进程,主要是提供了一些例如DLL注入这样的功能实现,自己的DLL注入到其他进程以后就可以用Local Hooking那些功能来达到目的了。

首先搞懂它那个TRACED_HOOK_HANDLE,简单说就是它可以记录钩子的相关信息。它本身的空间不是由安装钩子的函数来申请的,要由安装钩子的函数的调用者提供。一个要注意的是在DLL刚刚注入到目标进程的时候,C或者C++运行库不一定是已经初始化了的(我猜测如此),原因是我用new申请空间给TRACED_HOOK_HANDLE然后程序崩了。但是用HeapAlloc这个API来申请就没崩,当然也不排除是调试的时候不小心动了什么其他的地方……总之我这边试验的结果是这样。其他试验的结果,LocalAlloc和LocalFree可以用(类型我用了LPTR),GlobalAlloc和GlobalFree可以用(类型用了GPTR),VirtualAlloc和VirtualFree可以用(Alloc的时候用了COMMIT,Free的时候用了RELEASE,注意RELEASE配合的size是0),CoTaskMemAlloc和CoTaskMemFree不可以用,就算调用CoInitialize了以后也还是没法用,原因不明。这里面试验的只能说证明了Alloc成功,Free的时候程序没崩不过有没有失败不晓得……我是严格按照API文档做了的。另外说一下,不动态申请空间而是直接弄一个HOOK_TRACE_INFO型的全局变量,然后把它的地址送到函数里面去也是一种办法,试验的时候没问题。没有什么高要求的话用全局变量来搞还更方便。

然后就是安装和拆卸钩子的函数,LhInstallHook和LhUninstallHook,简单说就是一个是把函数给钩了一个是把函数给恢复了。LhInstallHook里面其他参数不用说,就说个那个callback是啥,它是一个指针,就有点像某些系统API提供回调功能的时候总是会留一个void*类型的指针说是“留给用户使用”,在回调的时候你可以获取这个指针。这里也是一样的,把原函数给钩到自己的目标函数以后,在这个目标函数里可以通过LhBarrierGetCallback来获取安装钩子的时候送进去的这个参数callback。

在“目标函数”里,调用原函数的操作似乎被正确处理了,不需要什么特别的动作也可以调用到原函数。应该是它修改了函数入口以后是跳转到一个特别的地方,这个地方的代码会判断是在什么地方调用的吧,从而不至于出现递归死循环(就是比如一个函数A你把它转向到B了,然后在B里调用了A,导致再一次转向到B……为了不出现这个问题就要使B里调用A的时候不会转向到B。当然在B里解除A函数入口的钩子然后再调用A不是什么好的办法,因为此举在多线程环境下不可重入)。

然后是LhSetInclusiveACL和LhSetExclusiveACL这两个函数,看到ACL会想到访问控制列表,它是控制哪些线程受影响哪些线程不受影响的。Inclusive是“包含的”而Exclusive是“排除的”。具体这两个什么情况下用我并不清楚,不过网上说不调用的话钩子不会生效,实际测试也确实是这样。不过LhSetExclusiveACL调用的时候那个Count送个0进去似乎也可以,不过List不能送0不然会无效。总之我的做法就是LhInstallHook以后LhSetExclusiveACL一下,Count送0,List随便送一个有效的数组即可。

通过这几个函数,差不多能把用来注入的DLL中所需要的功能实现了。然后是执行注入操作的EXE,也就是Loader。

它提供了RhInjectLibrary、RhCreateAndInject这两个函数可以提供注入功能。将DLL注入到目标进程以后,它会在DLL中查找_NativeInjectionEntryPoint@4函数并调用。这个地方就是给你执行安装钩子操作的地方。函数的声明在C++中写的话大概是这样

extern "C" __declspec(dllexport)
BOOL CALLBACK NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* pInfo)

参数里的UserData是调用RhInjectLibrary或者RhCreateAndInject的时候送进去的数据。这两个也可以直接用0:不需要送数据进去。

这个函数一定要有,不然RhInjectLibrary会报错不是在DllMain里面干这种事,是在NativeInjectionEntryPoint里面干

在NativeInjectionEntryPoint函数被调用的时候,进程的主线程(或者说进程也可以)是处于Suspend状态的,所以在NativeInjectionEntryPoint函数返回之前一定要调用RhWakeUpProcess函数。EasyHook就是这么设计的。调用了以后,进程才能继续执行。不然进程是处于挂起状态,函数返回以后就没有然后了。

弄个图简单说明一下过程

Setsumei

最后说一下为什么会突然间想到去弄这个吧。C84有个游戏叫メイドさんスレイヤー,中文系统上直接打开是乱码,AppLocale虽然可以解决,但是群里面有个人用的Windows 8的系统,说是没办法用AppLocale,于是我就想办法解决一下,其实也很简单就是CreateFontA那边设一下charset参数不要用Default而已。嗯……虽然弹出的MessageBox和窗口标题上的乱码没有解决不过至少游戏中是不乱码了。

发表评论