对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。知道了偏移量,按照第二步中介绍的步骤:移除内存保护、保存原始指针、注入自己的函数、还原内存保护,一气呵成。

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值