Inline HOOK API 改进版(hot-patching)

记得在之前写过一篇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函数,不建议强制使用这种方式,视情况而定。再者,在本文中没有涉及手工编写内嵌汇编代码,逻辑简单且更清晰,在效率上也较高一些,安全性方面也要好一些。不过之前的方法对于追究堆栈调用模型很有好处。

 

本文原理比较简单,就先介绍到此,欢迎大牛拍砖。

你可能感兴趣的:(api,byte,hook,DST,RAII,winapi)