对DirectX/COM接口的挂钩

 
对DirectX/COM接口的挂钩
 
 
         一般的挂钩(Hook)都是针对Windows API或消息的,而本文要讲的是如何挂钩一个DirectX/COM接口,有意思吧,请往下看,文中以DirectInput作为范例进行讲解。
 
 
         目标
         相比挂钩一个API调用,拦截一个COM接口的方法需要做更多的工作,如果我们要拦截的DLL已经被作者仔细检查过,只导出了类似create这样的接口函数,那么怎样才能达到我们挂钩的目的呢?
 
 
 
 
         从本质上来说,一个COM接口就是一张与其链接在一起的虚函数指针列表,所以,我们只需跟踪链接,并查看每一个节点,直到找到想要替换的函数指针即可。
 
 
         第一步
         从上图也可以看到,只有类似create的接口COM函数是可见的,由于DirectInputCreate函数会返回一个COM接口,所以就从它开始跟踪,在此,可以把DLL注入到目标程序的输入地址表(IAT)中。
 
 
         第二步
         如果目标程序调用了DirectInputCreate,我们的函数也会被调用,而且,会得到一个指针,其指向了虚函数表的指针,而这个虚函数表就是DirectInput的接口。
 
 
DECLARE_INTERFACE_(IDirectInputW, IUnknown)
{
   /*** IUnknown methods ***/
   STDMETHOD(QueryInterface)(THIS_ REFIID riid,
                             LPVOID * ppvObj) PURE;
   STDMETHOD_(ULONG,AddRef)(THIS) PURE;
   STDMETHOD_(ULONG,Release)(THIS) PURE;
 
   /*** IDirectInputW methods ***/
   STDMETHOD(CreateDevice)(THIS_ REFGUID,LPDIRECTINPUTDEVICEW *,
                           LPUNKNOWN) PURE;
   STDMETHOD(EnumDevices)(THIS_ DWORD,LPDIENUMDEVICESCALLBACKW,
                          LPVOID,DWORD) PURE;
   STDMETHOD(GetDeviceStatus)(THIS_ REFGUID) PURE;
   STDMETHOD(RunControlPanel)(THIS_ HWND,DWORD) PURE;
   STDMETHOD(Initialize)(THIS_ HINSTANCE,DWORD) PURE;
};
 
 
         第三步
         现在,就可用CreateDevice创建自己的设备了,在此会再次得到一个不同的虚函数指针表地址,它代表了设备。
 
 
 
 
         选择需要替换的方法,并在适当位置修改虚函数指针表以注入我们自己的函数。
 
 
         实现
         下面是实现步骤
 
 
         第一步
         要对一个API函数进行挂钩,可以使用SetWindowsHookEx这个Windows API,在此,我们创建了一个系统钩子,以监视启动的进程,检查其中是否有我们的目标程序。在确定之后,必须把它的输入模块名与想要进行替换的DLL进行比较,因为我们是对DirectInput进行挂钩,所以此项为DINPUT8.DLL。要找到此DLL,需遍历描述符。
 
 
//遍历每个输入描述符,在必要时重定向
   while ( pImportDesc->FirstThunk )
   {
      PSTR pszImportModuleName = MakePtr( PSTR, hModEXE,
                                          pImportDesc->Name);
 
      if ( lstrcmpi( pszImportModuleName, Hook->Name ) == 0 )
      {
         sprintf(dbBuffer,"Dll Found in module %s replace it/n",
                 Hook->Name );
         WriteToLog(dbBuffer);
         RedirectIAT( Hook, pImportDesc, (PVOID)hModEXE );
      }
 
      pImportDesc++;    //继续下一个输入描述符
   }
 
 
         找到之后,应使用VirtualQuery( pIAT, &mbi, sizeof(mbi) )从IAT中移除写保护,这样就可以写入到内存中了。在内存打开之后,还需遍历IAT查找入口项。
 
 
while ( pIteratingIAT->u1.Function )
{
   void* HookFn = 0;
 
   if ( !IMAGE_SNAP_BY_ORDINAL( pINT->u1.Ordinal ) )
   {
      PIMAGE_IMPORT_BY_NAME pImportName =
         MakePtr( PIMAGE_IMPORT_BY_NAME, pBaseLoadAddr,
                  pINT->u1.AddressOfData );
 
      //遍历挂钩函数
      SFunctionHook* FHook = DLLHook->Functions;
      while ( FHook->Name )
      {
         if ( lstrcmpi( FHook->Name, (char*)pImportName->Name ) == 0 )
         {
            sprintf(dbBuffer,"Hooked function: %s/n",
                    (char*)pImportName->Name );
            WriteToLog(dbBuffer);
            //在结构SFunctionHook中保存被替换的函数
            FHook->OrigFn = (unsigned long*)pIteratingIAT->u1.Function;
            HookFn = FHook->HookFn;
            break;
         }
 
         FHook++;
      }
 
   }
}
 
 
         现在,可替换为自己的函数了。
 
 
//在挂钩之后,替换IAT函数指针
if ( HookFn )
{
   //检查是代码还是数据
   //如果是代码,不应写入。
   if ( IsBadWritePtr( (PVOID)pIteratingIAT->u1.Function, 1 ) )
   {
      pIteratingIAT->u1.Function = (DWORD)HookFn;
   }
   else if ( osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS )
   {
      //检查“桩”是否在2GB内存地址空间之上
      if ( pIteratingIAT->u1.Function > (DWORD)0x80000000 )
         pIteratingIAT->u1.Function = (DWORD)HookFn;
   }
}
 
 
         最后就是还原内存属性。
 
 
VirtualProtect( pIAT, sizeof(PVOID) * cFuncs, flOldProtect, &flDontCare);
 
 
         第二步
         在CreateInterface方法内部,通过把我们的CreateDevice函数指针注入到虚函数表(Vtbl)中,就可以挂钩到COM接口内部,而虚函数表则由返回的ppvOut指针得到。
 
 
 DirectInput8Create_Type OldFn =
      (DirectInput8Create_Type)D3DHook.Functions[D3DFN_DirectInput8Create].OrigFn;
    HRESULT hr = OldFn( hinst, dwVersion, riidltf, ppvOut, punkOuter );
 
 
         解析此指针,直到找到指向虚函数表接口的指针,而在这个地址上,需要再次移除内存保护,以便注入自己的函数到表中,并且保存原函数指针。
         接下来,把我们自己的函数指针注入到虚函数表接口内CreateDevice函数指针的偏移量上,并还原内存保护。
         可以看到,CreateDevice是DirectInput接口的第4个方法,这意味着其在虚函数表内的偏移量为0x0C(指针乘3)
 
 
typedef struct IDirectInput *LPDIRECTINPUT;
 
#if !defined(__cplusplus) || defined(CINTERFACE)
#define IDirectInput_QueryInterface(p,a,b) (p)->lpVtbl->QueryInterface(p,a,b)
#define IDirectInput_AddRef(p) (p)->lpVtbl->AddRef(p)
#define IDirectInput_Release(p) (p)->lpVtbl->Release(p)
#define IDirectInput_CreateDevice(p,a,b,c) (p)->lpVtbl->CreateDevice(p,a,b,c)
#define IDirectInput_EnumDevices(p,a,b,c,d) (p)->lpVtbl->EnumDevices(p,a,b,c,d)
#define IDirectInput_GetDeviceStatus(p,a) (p)->lpVtbl->GetDeviceStatus(p,a)
#define IDirectInput_RunControlPanel(p,a,b) (p)->lpVtbl->RunControlPanel(p,a,b)
#define IDirectInput_Initialize(p,a,b) (p)->lpVtbl->Initialize(p,a,b)
#else
#define IDirectInput_QueryInterface(p,a,b) (p)->QueryInterface(a,b)
#define IDirectInput_AddRef(p) (p)->AddRef()
#define IDirectInput_Release(p) (p)->Release()
#define IDirectInput_CreateDevice(p,a,b,c) (p)->CreateDevice(a,b,c)
#define IDirectInput_EnumDevices(p,a,b,c,d) (p)->EnumDevices(a,b,c,d)
#define IDirectInput_GetDeviceStatus(p,a) (p)->GetDeviceStatus(a)
#define IDirectInput_RunControlPanel(p,a,b) (p)->RunControlPanel(a,b)
#define IDirectInput_Initialize(p,a,b) (p)->Initialize(a,b)
#endif
 
 
         在知道从何处注入之后,下面就要考虑如何实现了。可以查看CreateDevice在dinput.h头文件中的声明,会发现它与DirectX Help中的并不匹配。
 
 
HRESULT CreateDevice(
 
    REFGUID rguid,
    LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
    LPUNKNOWN pUnkOuter
);
 
 
         这是它在dinput.h头文件中的定义,所以我们必须添加第四个参数,其为接口指针,下面是完整的函数声明:
 
 
HRESULT __stdcall   PASCAL MyCreateDevice(LPVOID *ppvOut,REFGUID rguid,
    LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
    LPUNKNOWN pUnkOuter
 
 
         另外有一点非常重要,必须在声明中使用 __stdcall调用约定。__stdcall调用约定常用于调用Win32 API函数,被调用者负责清理堆栈;而__cdecl则是C和C++程序的默认调用约定,由调用者来负责清理堆栈,这可不是我们想要的。
         当查看此调用的反汇编时,会看到堆栈指针验证函数 _RTC_CheckEsp在对接口函数调用之后被调用。
 
 
if (lpdi->CreateDevice(GUID_SysKeyboard, &lpdikey, NULL)!=DI_OK)
00401365 mov         esi,esp
00401367 push        0   
00401369 push        offset lpdikey (4552C8h)
0040136E push        offset _GUID_SysKeyboard (44643Ch)
00401373 mov         eax,dword ptr [lpdi (4552C4h)]
00401378 mov         ecx,dword ptr [eax]
0040137A mov         edx,dword ptr [lpdi (4552C4h)]
00401380 push        edx 
00401381 mov         eax,dword ptr [ecx+0Ch]
00401384 call        eax 
00401386 cmp         esi,esp
00401388 call        _RTC_CheckEsp (4026A0h)
0040138D test        eax,eax
0040138F je          Game_Init+78h (401398h)
   return(0);
00401391 xor         eax,eax
00401393 jmp         Game_Init+107h (401427h)
 
//设置协作级
if (lpdikey->SetCooperativeLevel(main_window_handle,DDSCL_NORMAL);
 
 
         如果忘了把函数声明为 __stdcall,函数也会正常工作,但是esp指针测试将会失败,因为其会设置eax,并在函数调用之后对它进行测试。
 
 
         第三步
         现在,当创建设备时,调用会重定向到我们的CreateDevice函数中。另外,在我们调用原始函数之后,会在lplpDirectInputDevice中得到一个新的指针,它可以把我们直接带到设备的虚函数表中。
 
 
HRESULT hr = OldCreateDev(ppvOut,rguid,lplpDirectInputDevice,pUnkOuter);
 
 
         比如说,我们要替换GetDeviceState这个函数,为得到它的偏移量,必须在DInput.dll内部查找其定义,可以看到它是第10个方法,所以偏移量为0x24。知道了偏移量,按照第二步中介绍的步骤:移除内存保护、保存原始指针、注入自己的函数、还原内存保护,一气呵成。
 
阅读更多
个人分类: 挂钩
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭