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++实现,先写一个类。

 

  
  
  
  
  1. class InlineHookHolder   
  2. {   
  3. public:   
  4.     class _ReadWriteVPHolder   
  5.     {   
  6.     public:   
  7.         _ReadWriteVPHolder( void* addr, DWORD size ) : pAddr( addr ), dwSize( size )   
  8.         {    
  9.             VirtualProtect( pAddr, dwSize, PAGE_EXECUTE_READWRITE, &dwFlag );    
  10.         }   
  11.    
  12.         ~_ReadWriteVPHolder( void )   
  13.         {   
  14.             VirtualProtect( pAddr, dwSize, dwFlag, &dwFlag );   
  15.         }   
  16.    
  17.     private:   
  18.         void* pAddr;   
  19.         DWORD dwFlag;   
  20.         DWORD dwSize;   
  21.     };   
  22.    
  23.     InlineHookHolder( int dst_jmp_func, int src_hook_func ) :   
  24.         pSrcFunc( ( BYTE* )src_hook_func - 5 )   
  25.     {    
  26.         // raii vp Holder   
  27.         _ReadWriteVPHolder holder( pSrcFunc, 7 );   
  28.    
  29.         // jmp offset, contain 5 byte of itself   
  30.         ( int& )pSrcFunc[1] = ( int )dst_jmp_func - ( int )pSrcFunc - 5;   
  31.         pSrcFunc[ 0 ]       = 0xE9;     // far jmp   
  32.    
  33.         // mov edi, edi   
  34.         pSrcFunc[ 5 ]       = 0xEB;     // short jmp   
  35.         pSrcFunc[ 6 ]       = 0xF9;     // short jmp offset: -7   
  36.     }   
  37.    
  38.     ~InlineHookHolder( void )    
  39.     {   
  40.         // raii vp holder   
  41.         _ReadWriteVPHolder holder( pSrcFunc, 7 );   
  42.    
  43.         // 5 nop   
  44.         memset( pSrcFunc, 0x90, 5 );   
  45.    
  46.         // unhook mov edi, edi   
  47.         pSrcFunc[ 5 ] = 0x8B;   
  48.         pSrcFunc[ 6 ] = 0xFF;   
  49.     }   
  50.    
  51. private:   
  52.     BYTE* pSrcFunc;   
  53. };   

这个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,在其间的过程中都不需要操作。

写好了类,然后再写跳转到的自定义函数,代码如下:

 

  
  
  
  
  1. typedef WINBASEAPI HMODULE ( WINAPI *PLOADLIBA )( LPCSTR lpFileName );   
  2. typedef WINBASEAPI HMODULE ( WINAPI *PLOADLIBW )( LPCWSTR lpFileName );   
  3.    
  4. #define LOADER_CAST( T, ptr ) reinterpret_cast< T >( ( ( int )ptr ) + 2 );   
  5. #define WINAPI_FUNC( _ret, _name ) _ret WINAPI _name   
  6. #define DECLARE_HOOK_HOLDER( _T, holder, jmp_, src_ ) /   
  7.         _T jmp_##holder( ( int )jmp_, ( int )src_ );   
  8.    
  9. WINAPI_FUNC( HMODULE, MyLoadLibraryA )( LPCSTR lpFileName )   
  10. {   
  11.     if ( lpFileName == NULL )   
  12.         return NULL;   
  13.    
  14.     PLOADLIBA pLoader = LOADER_CAST( PLOADLIBA, LoadLibraryA );   
  15.     HMODULE   hMod    = pLoader( lpFileName );   
  16.     // code....   
  17.     return hMod;   
  18. }   

如上面代码,LOADER_CAST宏用于将API的入口地址加上2,避免又跳转到自定义hook函数里面了。

 

由于LoadLibraryA是__stdcall(WINAPI宏),会在内部ret时保持堆栈平衡,因此我们自定义的函数也保持这个规则,不然会导致堆栈不平衡。WINAPI_FUNC宏就是为了遵循这个规则而定义的,免得粗心忘了加__stdcall了。

 

DECLARE_HOOK_HOLDER宏是声明定义一个InlineHookHolder对象,将原函数和目标函数传入构造函数进行hook操作。我们可以将这个对象声明成全局的,这样在程序退出时调用析构进行unhook,_ReadWriteVPHolder类也是为了RAII的机制。

 

最后调用代码可以简单如下:

 

  
  
  
  
  1. DECLARE_HOOK_HOLDER( InlineHookHolder, _inline_hook_a, MyLoadLibraryA, LoadLibraryA );   
  2. int main( void )   
  3. {   
  4.     LoadLibraryA( "d3d9.dll" ); // 假如load这个dll   
  5.     return 0;   
  6. }   

当然在MyLoadLibraryA里就可以做一些我们想做的事情了,比如检测你加载的DLL是否合法等,这对于比较简单的游戏反外挂上有一定的作用。本文就只抛砖引玉介绍下原理吧。

 

 

与之前的版本比较,这种方法只能应用于预留了5个nop和mov edi,edi这7个字节的API函数,不建议强制使用这种方式,视情况而定。再者,在本文中没有涉及手工编写内嵌汇编代码,逻辑简单且更清晰,在效率上也较高一些,安全性方面也要好一些。不过之前的方法对于追究堆栈调用模型很有好处。

 

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

你可能感兴趣的:(api,职场,c/c++,inline,hook,休闲)