记得在之前写过一篇hook api的文章(C/C++ HOOK API(原理深入剖析之-LoadLibraryA),那篇文章主要原理是构造一块代码字节,将LoadLibraryA函数的前面16个字节给修改,然后跳转到自定义的函数中。要调用正常的函数时,又将其unHook,这样一来再一次调用中,有一次unhook和一次hook,操作显得过于频繁。而且hook与unhook当时设计成了thiscall,因此维护传递this的寄存器(通常是ecx)就成了必然,再加上参数的传递,__Inline_Hook_Func函数的逻辑就显得有点复杂和臃肿。
就以上情况,本文打算较之前的逻辑做个改进,抛弃掉this和参数的维护,利用windows为多数API预留的前五个字节的nop空间和mov edi, edi占用的两个字节来实现inline hook。即所谓的hot-patching技术,运行时修改函数的行为,同时不破坏函数的主体逻辑,因此免去了hook与unhook不断切换,在线程安全上也有很好的效果。
本文还是以LoadLibraryA函数为例,先看LoadLibraryA的反汇编:
7602285F nop
76022860 nop
76022861 nop
76022862 nop
76022863 nop
76022864 mov edi,edi
76022866 push ebp
76022867 mov ebp,esp
76022869 cmp dword ptr [ebp+8],0
7602286D push ebx
7602286E push esi
7602286F push edi
76022870 je 7602288A
76022872 push 760228A0h
76022877 push dword ptr [ebp+8]
7602287A call dword ptr ds:[75FD12E4h]
76022880 pop ecx
76022881 pop ecx
76022882 test eax,eax
76022884 je 7603F39F
7602288A push 0
7602288C push 0
7602288E push dword ptr [ebp+8]
76022891 call 76022859
76022896 pop edi
76022897 pop esi
76022898 pop ebx
76022899 pop ebp
7602289A ret 4
如上,前面红色的部分即是可以灵活操作的部分,mov edi,edi两个字节可以替换成一个short jmp,5个nop字节可以替换成一个长跳。
蓝色的0x76022864地址是LoadLibraryA的入口,如果将这里改成short jmp后,要调用正常的LoadLibraryA,则只需要在此基础上加2个字节的偏移进行call即可。清楚了原理,先动手吧,用C++实现,先写一个类。
- class InlineHookHolder
- {
- public:
- class _ReadWriteVPHolder
- {
- public:
- _ReadWriteVPHolder( void* addr, DWORD size ) : pAddr( addr ), dwSize( size )
- {
- VirtualProtect( pAddr, dwSize, PAGE_EXECUTE_READWRITE, &dwFlag );
- }
- ~_ReadWriteVPHolder( void )
- {
- VirtualProtect( pAddr, dwSize, dwFlag, &dwFlag );
- }
- private:
- void* pAddr;
- DWORD dwFlag;
- DWORD dwSize;
- };
- InlineHookHolder( int dst_jmp_func, int src_hook_func ) :
- pSrcFunc( ( BYTE* )src_hook_func - 5 )
- {
- // raii vp Holder
- _ReadWriteVPHolder holder( pSrcFunc, 7 );
- // jmp offset, contain 5 byte of itself
- ( int& )pSrcFunc[1] = ( int )dst_jmp_func - ( int )pSrcFunc - 5;
- pSrcFunc[ 0 ] = 0xE9; // far jmp
- // mov edi, edi
- pSrcFunc[ 5 ] = 0xEB; // short jmp
- pSrcFunc[ 6 ] = 0xF9; // short jmp offset: -7
- }
- ~InlineHookHolder( void )
- {
- // raii vp holder
- _ReadWriteVPHolder holder( pSrcFunc, 7 );
- // 5 nop
- memset( pSrcFunc, 0x90, 5 );
- // unhook mov edi, edi
- pSrcFunc[ 5 ] = 0x8B;
- pSrcFunc[ 6 ] = 0xFF;
- }
- private:
- BYTE* pSrcFunc;
- };
这个InlineHookHolder构造时,便将目标函数进行hook操作,这个操作将mov edi,edi替换成jmp XX,这里是相对偏移,偏移量为7字节(5个nop字节加上本身2个字节),所以有:
pSrcFunc[ 5 ] = 0xEB; // short jmp
pSrcFunc[ 6 ] = 0xF9; // short jmp offset: -7
0xEB即是short jmp的机器码,0xF9即偏移,这里是负数,向前偏移7个字节。
至于5个nop即替换成了jmp 0x???????。因此有:
( int& )pSrcFunc[1] = ( int )dst_jmp_func - ( int )pSrcFunc - 5; // 跳转的偏移,目标地址与当前地址的差值+jmp本身的5个字节
pSrcFunc[ 0 ] = 0xE9; // 长jmp 的机器码
在构造函数修改之后,析构函数负责unhook,在其间的过程中都不需要操作。
写好了类,然后再写跳转到的自定义函数,代码如下:
- typedef WINBASEAPI HMODULE ( WINAPI *PLOADLIBA )( LPCSTR lpFileName );
- typedef WINBASEAPI HMODULE ( WINAPI *PLOADLIBW )( LPCWSTR lpFileName );
- #define LOADER_CAST( T, ptr ) reinterpret_cast< T >( ( ( int )ptr ) + 2 );
- #define WINAPI_FUNC( _ret, _name ) _ret WINAPI _name
- #define DECLARE_HOOK_HOLDER( _T, holder, jmp_, src_ ) /
- _T jmp_##holder( ( int )jmp_, ( int )src_ );
- WINAPI_FUNC( HMODULE, MyLoadLibraryA )( LPCSTR lpFileName )
- {
- if ( lpFileName == NULL )
- return NULL;
- PLOADLIBA pLoader = LOADER_CAST( PLOADLIBA, LoadLibraryA );
- HMODULE hMod = pLoader( lpFileName );
- // code....
- return hMod;
- }
如上面代码,LOADER_CAST宏用于将API的入口地址加上2,避免又跳转到自定义hook函数里面了。
由于LoadLibraryA是__stdcall(WINAPI宏),会在内部ret时保持堆栈平衡,因此我们自定义的函数也保持这个规则,不然会导致堆栈不平衡。WINAPI_FUNC宏就是为了遵循这个规则而定义的,免得粗心忘了加__stdcall了。
DECLARE_HOOK_HOLDER宏是声明定义一个InlineHookHolder对象,将原函数和目标函数传入构造函数进行hook操作。我们可以将这个对象声明成全局的,这样在程序退出时调用析构进行unhook,_ReadWriteVPHolder类也是为了RAII的机制。
最后调用代码可以简单如下:
- DECLARE_HOOK_HOLDER( InlineHookHolder, _inline_hook_a, MyLoadLibraryA, LoadLibraryA );
- int main( void )
- {
- LoadLibraryA( "d3d9.dll" ); // 假如load这个dll
- return 0;
- }
当然在MyLoadLibraryA里就可以做一些我们想做的事情了,比如检测你加载的DLL是否合法等,这对于比较简单的游戏反外挂上有一定的作用。本文就只抛砖引玉介绍下原理吧。
与之前的版本比较,这种方法只能应用于预留了5个nop和mov edi,edi这7个字节的API函数,不建议强制使用这种方式,视情况而定。再者,在本文中没有涉及手工编写内嵌汇编代码,逻辑简单且更清晰,在效率上也较高一些,安全性方面也要好一些。不过之前的方法对于追究堆栈调用模型很有好处。
本文原理比较简单,就先介绍到此,欢迎大牛拍砖。