65.9K
CodeProject 正在变化。 阅读更多。
Home

挂钩真正的 COM 对象: 拦截 IHTMLDocument3 函数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (18投票s)

2011年12月13日

CPOL

10分钟阅读

viewsIcon

55086

downloadIcon

1471

本文介绍了一个关于 Hook 真实 COM 对象的问题,并提供了解决方案。

目录

  1. 引言
  2. 动机
  3. Hook COM 接口 IHTMLDocument3
  4. 代码中的问题分析
    1. 解决方案 #1: 使用裸函数 (Naked Function)
    2. 解决方案 #1: 局限性
    3. 解决方案 #2: 实现 TearoffThunk12 函数以查找真实方法
    4. 解决方案 #2: 局限性
  5. 关注点
  6. 基于该技术实现的软件
  7. 参考文献
  8. 历史

引言

拦截 COM 组件方法的基本概念非常简单易懂。正如许多文章 [1, 2, 3, 4] 所解释的,您可以通过修改组件的虚函数表 (Virtual Table) 中的某个元素来将函数重定向到您的处理程序。理论上,这种机制应该适用于所有 COM 组件。然而,在实际操作中,您可能会在实现和调试这些技术时遇到一些奇怪的错误。例如,拦截 IHTMLDocument3::attachEvent 方法会导致运行时错误。
本文分析了这些错误发生的原因,并提出了相应的解决方案。

动机

我决定写这篇文章来分享我在实现我的一个应用程序时获得的经验和知识。一个月前,我开始开发一个应用程序,用于禁用阻止复制粘贴功能的脚本代码。例如,正如一篇文章 [5] 所述,有些网站会注册一些脚本函数来阻止用户复制和粘贴。基本上,大多数这些脚本都可以通过移除注册的事件处理程序来禁用。然而,如果使用 attachEvent 函数注册了一个匿名函数作为事件处理程序,那么该处理程序就无法被注销。这是因为 detachEvent 函数需要已注册函数的标识符,而匿名函数不提供这个标识符。
为了解决这个问题,我决定编写代码来 Hook COM 接口 IHTMLDocument3::attachEvent

Hook COM 接口 IHTMLDocument3

由于 Hook COM 接口是一个有许多文章讨论的主题,本节只包含代码及其细节,而不涉及基本概念和背景。
如果您想详细了解这个基本思想,上述文章 [1, 2, 3, 4] 将是很好的参考。

要拦截 IHTMLDocument3 接口的方法,首先要做的是获取浏览器的接口。由于我正在使用 Web Browser ActiveX 控件,我选择了 'DownloadBegin' 事件来获取接口。通过创建一个 'DownloadBegin' 的事件处理程序,您可能会得到 'OnDownloadBeginExplorer1' 函数。使用以下代码,您可以在每次网页加载时获得 IHTMLDocument3 接口。并且,获取到的接口将在网页重新加载之前一直可用。

void CxxxDlg::OnDownloadBeginExplorer1()
{
	HRESULT hr;
	IDispatch* lpDisp = m_web.GetDocument();
	if(lpDisp){
		IHTMLDocument3* pDoc3 = 0;
		hr = lpDisp->QueryInterface( IID_IHTMLDocument3, (void**)&pDoc3);
		if( SUCCEEDED(hr) && pDoc3 ) {
			HookInterface1( pDoc3 );
			pDoc3->Release();
		} else {
			OutputDebugString("Failed to query interface");
		}
	} else {
		OutputDebugString("Failed to get document");
	}
}

之后,下一步是构建 IHTMLDocument3 的 C 风格接口结构。虽然您可以使用方法偏移量在没有 C 风格接口的情况下 Hook 方法,但我使用 C 接口是为了使代码更易读。

总之,下面的结构是 IHTMLDocument3 结构的一部分。由于我只想 Hook attachEvent 方法,所以该结构不包含所有方法。
要制作一个用于 Hook 的 C 风格接口,您可以按顺序复制 IDispatchVtbl 和您想要 Hook 的目标接口的所有方法。

typedef struct IHookHTMLDocument3Vtbl
{
	BEGIN_INTERFACE

	HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
	    IDispatch * This,
	    /* [in] */ REFIID riid,
	    /* [iid_is][out] */ void **ppvObject);

	ULONG ( STDMETHODCALLTYPE *AddRef )(
	    IDispatch * This);

	ULONG ( STDMETHODCALLTYPE *Release )(
	    IDispatch * This);

	HRESULT ( STDMETHODCALLTYPE *GetTypeInfoCount )(
	    IDispatch * This,
	    /* [out] */ UINT *pctinfo);

	HRESULT ( STDMETHODCALLTYPE *GetTypeInfo )(
	    IDispatch * This,
	    /* [in] */ UINT iTInfo,
	    /* [in] */ LCID lcid,
	    /* [out] */ ITypeInfo **ppTInfo);

	HRESULT ( STDMETHODCALLTYPE *GetIDsOfNames )(
	    IDispatch * This,
	    /* [in] */ REFIID riid,
	    /* [size_is][in] */ LPOLESTR *rgszNames,
	    /* [in] */ UINT cNames,
	    /* [in] */ LCID lcid,
	    /* [size_is][out] */ DISPID *rgDispId);

	/* [local] */ HRESULT ( STDMETHODCALLTYPE *Invoke )(
	    IDispatch * This,
	    /* [in] */ DISPID dispIdMember,
	    /* [in] */ REFIID riid,
	    /* [in] */ LCID lcid,
	    /* [in] */ WORD wFlags,
	    /* [out][in] */ DISPPARAMS *pDispParams,
	    /* [out] */ VARIANT *pVarResult,
	    /* [out] */ EXCEPINFO *pExcepInfo,
	    /* [out] */ UINT *puArgErr);


	/****************************************************************/
	/****************************************************************/
	/****************************************************************/
	// Now you can copy and paste methods from the object what you would like to hook.

	HRESULT (STDMETHODCALLTYPE * releaseCapture)(
	    IHTMLDocument3* This);

	HRESULT (STDMETHODCALLTYPE * recalc)(
	    IHTMLDocument3* This,
	    /* [in][defaultvalue] */ VARIANT_BOOL fForce);

	HRESULT (STDMETHODCALLTYPE * createTextNode)(
	    IHTMLDocument3* This,
	    /* [in] */ BSTR text,
	    /* [out][retval] */ IHTMLDOMNode **newTextNode);

	HRESULT (STDMETHODCALLTYPE * get_documentElement)(
	    IHTMLDocument3* This,
	    /* [out][retval] */ IHTMLElement **p);

	HRESULT (STDMETHODCALLTYPE * get_uniqueID)(
	    IHTMLDocument3* This,
	    /* [out][retval] */ BSTR *p);

	HRESULT (STDMETHODCALLTYPE * attachEvent)(
	    IHTMLDocument3* This,
	    /* [in] */ BSTR event,
	    /* [in] */ IDispatch *pDisp,
	    /* [out][retval] */ VARIANT_BOOL *pfResult);

	END_INTERFACE
// org
//} IDispatchVtbl;
} IHookHTMLDocument3Vtbl;

// org
//interface IDispatch
interface IHookHTMLDocument3
{
	//CONST_VTBL struct IDispatchVtbl *lpVtbl;
	CONST_VTBL struct IHookHTMLDocument3Vtbl *lpVtbl;
};

接下来是替换方法。通过使用我们创建的结构,您可以轻松地访问方法的地址,而无需计算偏移量。为了覆盖虚函数表中的元素,使用了 VirtualProtect API 来使内存区域可写。HookInterface1 函数完成了这项工作。

void CIETestDlg::HookInterface1(IHTMLDocument3 *pDoc3)
{
	IHookHTMLDocument3* pHookDoc3 = (IHookHTMLDocument3*)pDoc3;
	CString s;
	s.Format("Address of Vtbl : %08X, FuncAddr : %08X, mshtml.dll : %08X",
		pHookDoc3->lpVtbl, pHookDoc3->lpVtbl->attachEvent, 
				GetModuleHandle("mshtml.dll") );
	OutputDebugString(s);

	if( (PFNATTACHEVENT)pHookDoc3->lpVtbl->attachEvent != 
				(PFNATTACHEVENT)New_attachEvent ) {
		DWORD dwOldProt = 0;
		if( VirtualProtect(&(pHookDoc3->lpVtbl->attachEvent), 
				4, PAGE_EXECUTE_READWRITE, &dwOldProt) ) {
			g_pfnOrgAttachEvent = (PFNATTACHEVENT)pHookDoc3->
							lpVtbl->attachEvent;
			g_ppfnOrgAttachEvent = (PFNATTACHEVENT*)&
					(pHookDoc3->lpVtbl->attachEvent);
			pHookDoc3->lpVtbl->attachEvent = New_attachEvent;
			VirtualProtect(&(pHookDoc3->lpVtbl->attachEvent), 
						4, dwOldProt, &dwOldProt);
		} else {
			OutputDebugString("Failed to unprotect the memory area");
		}
	} else {
		OutputDebugString("This has been already hooked");
	}
}

下面的函数是 attachEvent 方法的新处理程序。它只是显示一个消息框,以检查函数是否已被 Hook。

HRESULT STDMETHODCALLTYPE New_attachEvent(
			IHTMLDocument3* This,
            /* [in] */ BSTR event,
            /* [in] */ IDispatch *pDisp,
            /* [out][retval] */ VARIANT_BOOL *pfResult)
{
	CString szEvent ( event );
	AfxMessageBox("The attachEvent method is intercepted: event = " + szEvent);
	return g_pfnOrgAttachEvent( This, event, pDisp, pfResult );
}

理论上,这应该是有效的。然而,正如我在本文开头提到的,它并没有正常工作。虽然表面上看起来工作正常,但在您选择一个对象并将鼠标指针移到其上时,应用程序会崩溃。下图显示了因此原因导致的错误。

memory fault

代码中的问题分析

为了找出代码中的问题,第一步是使用调试器检查程序。我在 New_attachEvent 函数中插入了一个断点 (int 3h),如下面的代码所示,并在 OllyDbg 中运行它。

HRESULT STDMETHODCALLTYPE New_attachEvent(
			IHTMLDocument3* This,
            /* [in] */ BSTR event,
            /* [in] */ IDispatch *pDisp,
            /* [out][retval] */ VARIANT_BOOL *pfResult)
{
	// trap it in the debugger
	__asm int 3h

	return g_pfnOrgAttachEvent( This, event, pDisp, pfResult );
}

应用程序将在 'int 3h' 处停止,下面的截图显示了包括堆栈和代码的状态。

Ollydbg: Application has been stopped in New_attachEvent

正如您所看到的,堆栈上有一个事件名称 "oncontextmenu"(在屏幕的右下角)。然而,如果您选择一个对象并将鼠标指针移到其上,应用程序将显示一个不同的堆栈,其中不包含事件名称。下图显示了该状态。

Ollydbg: the function is also called from other places

这怎么可能呢?为了详细了解发生了什么,我反汇编了 attachEvent 方法的原始代码。正如您可能已经知道的,我在 HookInterface1 函数中打印了原始方法的地址,以便轻松找到与 IHTMLDocument3 接口关联的代码地址。

下面的反汇编代码是 IHTMLDocument3::attachEvent 指向的函数。我从 IDA 反汇编器复制了它。

.text:63988944 ; void __stdcall TearoffThunk12(void)
.text:63988944 ?TearoffThunk12@@YGXXZ proc near        ; DATA XREF: .data:64030540 o
.text:63988944                                         ; .data:6403BAC0 o
.text:63988944
.text:63988944 arg_0           = dword ptr  4
.text:63988944
.text:63988944 ; FUNCTION CHUNK AT .text:63A40595 SIZE 00000008 BYTES
.text:63988944
.text:63988944                 mov     eax, [esp+arg_0]
.text:63988948                 push    eax
.text:63988949                 test    dword ptr [eax+1Ch], 1000h
.text:63988950                 jnz     loc_63A40595
.text:63988956
.text:63988956 loc_63988956:                           ; 
		CODE XREF: TearoffThunk12(void)+B7C54 j
.text:63988956                 add     eax, 0Ch
.text:63988959                 mov     ecx, [eax]
.text:6398895B                 mov     [esp+4+arg_0], ecx
.text:6398895F                 mov     ecx, [eax+4]
.text:63988962                 mov     ecx, [ecx+30h]
.text:63988965                 pop     eax
.text:63988966                 mov     word ptr [eax+20h], 0Ch
.text:6398896C                 jmp     ecx
.text:6398896C ?TearoffThunk12@@YGXXZ endp

显然,TearoffThunk12 似乎不包含任何与附加事件处理程序相关的代码。换句话说,TearoffThunk12 函数并不执行 attachEvent 的实际工作。它似乎只是检查一些东西然后跳转到 attachEvent 的真实处理程序。经过广泛的研究,我发现 TearoffThunk12 函数与 tear-off 接口有关,它是在运行时请求时动态创建的接口 [6]。具体来说,通常采用 tear-off 接口来实现在运行时很少使用的接口,以节省内存空间。

有了以上信息,我们现在可以得出结论:IHTMLDocument3 接口是一个 tear-off 接口。而且,由于我们观察到当用户将鼠标指针移到选定的对象上时,其他接口会调用 TearoffThunk12,因此可能存在另一个共享 IHTMLDocument3 接口的虚函数表的接口。

通过反向工程 TearoffThunk12 的调用者,我发现 IHTMLElement 接口的一个方法调用了 TearoffThunk12。我通过使用下面的代码进行了验证。

void CIETestDlg::OnBtnshowvtbl()
{
	HRESULT hr, hr2 ;
	IDispatch* lpDisp = m_web.GetDocument();
	if(lpDisp){
		IHTMLDocument3* pDoc3 = 0;
		IHTMLDocument2* pDoc2 = 0;
		hr2 = lpDisp->QueryInterface( IID_IHTMLDocument2, (void**)&pDoc2);
		hr = lpDisp->QueryInterface( IID_IHTMLDocument3, (void**)&pDoc3);
		if( SUCCEEDED(hr) && SUCCEEDED(hr2) && pDoc3 && pDoc2) {
			IHTMLElement* pElem = 0;
			hr = pDoc2->get_body( (IHTMLElement**)&pElem );
			if( SUCCEEDED(hr) && pElem ) {
				CString s;
				s.Format("%08X  %08X  = 
				pDoc2\n%08X  %08X  = pDoc3\n%08X  %08X  = pElem",
					pDoc2, ((IHookHTMLDocument3*)pDoc2)->lpVtbl,
					pDoc3, ((IHookHTMLDocument3*)pDoc3)->lpVtbl,
					pElem, ((IHookHTMLDocument3*)pElem)->lpVtbl );
				AfxMessageBox(s);
			} else {
				OutputDebugString("Failed to get body");
			}
		} else {
			OutputDebugString("Failed to query interface");
		}
	} else {
		OutputDebugString("Failed to get document");
	}
}

如下面的图片所示,非常有趣的是 pDoc3pElem 都共享上面的代码中的虚函数表 lpVtbl

document and body objects share the virtual table

下图显示了测试结果,意味着 IHTMLElementIHTMLDocument3 拥有相同的虚函数表。

the structure of virtual table in this case

因此,由于 pDoc3pElem 共享相同的 lpVtbl,当我们改变 IHTMLDocument3 (pDoc3) 的一个元素时,IHTMLElement (pElem) 也会受到影响。
然而,原始函数的参数数量可能彼此不同。例如,IHTMLDocument3::attachEvent 有 4 个参数,而 IHTMLElement::put_id 方法(对应于 attachEvent 方法)有 2 个参数。在这种情况下,如果调用 IHTMLElement::put_id,堆栈将会损坏并导致运行时错误(内存故障)。

解决方案 #1: 使用裸函数 (Naked Function)

在上一节中,我们发现了问题以及 COM 组件内部发生的情况。在本节中,我建议第一个解决方案,即使用裸函数作为处理程序。

正如您在课堂上学到的,函数通常有 prologue 代码来创建自己的函数帧。同样,也有 epilogue 代码,它根据调用约定 (cdecl, stdcall, fastcall 等) 和参数数量移除帧并恢复堆栈。

因此,由于由 TearoffThunk12 函数调用的原始函数的参数数量不确定,我们不能使用具有 epilogue 和 prologue 代码的函数。这是因为 prologue 和 epilogue 代码会在原始函数与新处理程序的定义不同时弄乱堆栈。

为了解决这个问题,我使用裸函数作为处理程序,如下面的代码所示。

__declspec( naked ) HRESULT New_attachEvent_naked()
{
	__asm {
		mov eax, dword ptr[esp+8]
		push eax
		call New_attachEvent_Internal
	}
	__asm {
		jmp g_pfnOrgAttachEvent
	};
}

void __stdcall New_attachEvent_Internal(BSTR event)
{
	WCHAR* pwstrEvent = (WCHAR*)event;
	if( pwstrEvent ) {
		//
		// it can be called from other methods.
		//
		// The below code makes sure the pwstrEvent is BSTR and readable.
		//
		MEMORY_BASIC_INFORMATION mbi;
		memset( &mbi, 0, sizeof(mbi) );
		if( VirtualQuery( pwstrEvent, &mbi, sizeof(mbi) ) > 0 ) {
			DWORD dwBaseAddress = (DWORD)mbi.BaseAddress;
			DWORD dwstrEvent = (DWORD)pwstrEvent;

			if( dwstrEvent == 0 || dwBaseAddress == 0 || 
						mbi.RegionSize == 0 ) {
				return ;
			}

			if( ((DWORD)dwBaseAddress < (DWORD)dwstrEvent) && 
				((DWORD)dwstrEvent < (DWORD)(dwBaseAddress + 
				mbi.RegionSize - 1)) &&
				(mbi.State == MEM_COMMIT) &&
				(	((mbi.Protect & PAGE_EXECUTE_READWRITE) == 
						PAGE_EXECUTE_READWRITE) ||
					((mbi.Protect & PAGE_EXECUTE_READ) == 
						PAGE_EXECUTE_READ) ||
					((mbi.Protect & PAGE_EXECUTE_WRITECOPY) == 
						PAGE_EXECUTE_WRITECOPY) ||
					((mbi.Protect & PAGE_READONLY) == 
						PAGE_READONLY) ||
					((mbi.Protect & PAGE_READWRITE) == 
						PAGE_READWRITE) ||
					((mbi.Protect & PAGE_WRITECOPY) == 
						PAGE_WRITECOPY)  ) &&
				(	!((mbi.Protect & PAGE_GUARD) == PAGE_GUARD)	
				)
			   )
			{
				if( pwstrEvent[0] == 'o' && pwstrEvent[1] == 'n' ) {
					CString szEvent(pwstrEvent);

					MessageBox( NULL, "The attachEvent method 
					is intercepted: event = " + szEvent,
						"New_attachEvent_Internal",
						MB_ICONINFORMATION|MB_OK );
				}
			}
		}
	}
}

它只是压入第一个参数 (esp+8) 并调用 New_attachEvent_Internal 函数。New_attachEvent_Internal 函数使用 VirtualQuery API 来验证参数。此外,您可以通过检查事件名称的前 2 个字节来确保它是对 attachEvent 方法的调用。如果它以 "on" 开头,则必须是 attachEvent 方法的请求。所有这些验证方法都已在 New_attachEvent_Internal 函数中实现。下面的截图显示它工作正常。

now attachEvent has been successfully intercepted

解决方案 #1: 局限性

虽然现在它工作得很好,但您应该知道它不能保证将来也会工作得很好。如果接口发生更改,或者出现一个与 IHTMLDocument3 非常相似但又不完全相同的新接口,并且共享 IHTMLDocument3 的虚函数表,这可能会导致其他内存故障问题。在这些情况下,您可能需要添加更健壮的内存检查代码来防止错误。

解决方案 #2: 实现 TearoffThunk12 函数以查找真实方法

另一个解决方案是通过实现 TearoffThunk12 函数的代码来查找真实方法。基本思路非常简单。根据以上发现,TearoffThunk12 使用接口实例的地址成功调用了真实方法。因此,如果我们跟踪并执行该函数所做的操作,就可以找到真实方法的地址。

下面的代码是 TearoffThunk12 函数,我添加了一些描述。

61198944   8B4424 04        MOV EAX,DWORD PTR SS:[ESP+4] 	// eax = this
61198948   50               PUSH EAX 			// push this
61198949   F740 1C 00100000 TEST DWORD PTR DS:[EAX+1C],1000	// if( *(this + 0x1C) & 
							// 0x1000 != 0 )
61198950   0F85 3F7C0B00    JNZ mshtml.61250595		// jmp mshtml.61250595
		1.	if( *(this + 0x1C) == 0x1000 ) then goto mshtml.61250595 
								(return code)

61198956   83C0 0C          ADD EAX,0C			// eax = this+0xC
61198959   8B08             MOV ECX,DWORD PTR DS:[EAX]		// ecx = *(this+0xC)
6119895B   894C24 08        MOV DWORD PTR SS:[ESP+8],ECX	// orgparam = *(this+0xC)
		2.	Replace original this pointer in the stack with '*(this + 0xC)'

6119895F   8B48 04          MOV ECX,DWORD PTR DS:[EAX+4]	// ecx = *(this +0xC + 0x4)
61198962   8B49 30          MOV ECX,DWORD PTR DS:[ECX+30]	// ecx = * 
							//( *(this + 0x10) + 0x30 ) )
61198965   58               POP EAX
61198966   66:C740 20 0C00  MOV WORD PTR DS:[EAX+20],0C	// *(this+0x20) = 0xC
		3.	*(this + 0x20) = 0xC;

6119896C   FFE1             JMP ECX				// jmp to real handler
					// (=ecx = *( *(this + 0x10) + 0x30 ) ) )
		4.	jmp *( *(this + 0x10) + 0x30 );

根据上面的反汇编代码,TearoffThunk12 函数可以总结为下面的 C 函数。

__declspec( naked ) void TearoffThunk12(IDispatch* pThis)
{
	if( (*(pThis + (0x1C/sizeof(IDispatch*))) & 0x1000) == 0 ) {
		jmp Return_Thunk;
	}

	*pThis = *(pThis + (0xC/sizeof(IDispatch*)));
	*(pThis + (0x20/sizeof(IDispatch*))) = 0xC;

	jmp *( *(pThis + (0x10/sizeof(IDispatch*))) + (0x30/sizeof(IDispatch*)) );
}

关键在于它使用 "*(*(this+0x10)+0x30)" 来调用原始函数。另一个有趣的地方是反汇编代码中的数字 2。它将原始的 this 指针在堆栈上替换为 *(this + 0xC)。您可能知道,所有 COM 方法的第一个参数都是 "this 指针"。将第一个参数更改为 "*(this + 0xC)" 的意思是 tear-off 对象将原始对象存储在 "*(this + 0xC)" 中。

总之,通过检查该内存地址,我发现它是真实对象的虚函数表。组件的完整图示显示在下面的图片中。

the structure of an instance of tear-off interface

对象的起始地址表示 'pDoc3' 指针本身。它有几个项,其中一个就是 IHTMLDocument3 的真实对象及其虚函数表。在其虚函数表中,第 12 个方法就是我们要找的。

基于这些发现,我编写了一个函数,通过修改真实对象的虚函数表元素来拦截方法。

void CIETestDlg::HookInterface3(IHTMLDocument3 *pDoc3)
{
	CString s;
	IHookHTMLDocument3* pHookDoc3 = (IHookHTMLDocument3*)pDoc3;
	s.Format("Address of Vtbl : %08X FuncAddr : %08X",
		pHookDoc3->lpVtbl,
		pHookDoc3->lpVtbl->attachEvent );
	OutputDebugString(s);

	LPVOID* lpObj = (LPVOID*)pDoc3;

	// get the mask
	DWORD* pdwMask = (DWORD*)lpObj;
	DWORD dwMask;
	pdwMask = pdwMask + (0x1C / sizeof(DWORD));
	dwMask = *pdwMask;

	if( dwMask & 0x1000 ) {
		OutputDebugString("Something wrong - Error");
	} else {
		LPVOID* lpAddress = NULL;
		LPVOID* lpAddressElem = NULL;

		//
		// Find the address of function
		//  exp: *( *(this + 0x10) + 0x30 )
		//
		// this + 0x10
		lpAddress = lpObj + (0x10 / sizeof(LPVOID));
		// *(this + 0x10)
		lpAddress = (LPVOID*)*lpAddress;
		// *(this + 0x10) + 0x30
		lpAddress = lpAddress + (0x30 / sizeof(LPVOID));
		// *( *(this + 0x10) + 0x30 )
		lpAddressElem = lpAddress; // Save it to hook in later.
		lpAddress = (LPVOID*)*lpAddress;

		if( (PFNATTACHEVENT)lpAddress != (PFNATTACHEVENT)New_attachEvent ) {
			///
			//
			// try to replace the function address
			//
			///
			DWORD dwOldProt = 0;
			if( VirtualProtect(lpAddressElem, 4, 
				PAGE_EXECUTE_READWRITE, &dwOldProt) ) {
				g_pfnOrgAttachEvent = (PFNATTACHEVENT)lpAddress;
				g_ppfnOrgAttachEvent = 
					(PFNATTACHEVENT*)lpAddressElem;
				*lpAddressElem = New_attachEvent;
				VirtualProtect(lpAddressElem, 
					4, dwOldProt, &dwOldProt);
			} else {
				OutputDebugString
				("Failed to unprotect the memory area");
			}
		} else {
			OutputDebugString("This has been already hooked");
		}
	}
}

解决方案 #2: 局限性

虽然第二个解决方案看起来更令人信服和准确,但接口的内部实现将来可能会发生变化。

此外,值得注意的是,我在拥有 IE6、IE7、IE8 和 IE9 的系统上测试了第二个解决方案。(在 Windows XP 和 7 的所有已发布 IE 版本上运行良好。)在所有测试中,第二个解决方案都完美地实现了我们的目标,没有任何小的错误。同样,很难想象 Internet Explorer 的基本原理会发生变化。然而,没有人能肯定地说它不会改变。

因此,为了避免在新版本的 Internet Explorer 中出现潜在错误,您应该验证该方法和接口是否使用了 TearoffThunk12 函数。这可以通过将上述反汇编代码与内存中的代码进行比较来完成。

有趣的点

关于这个主题(拦截某事物)有很多文章。然而,据我所知,大多数文章都没有涵盖非常复杂的实际示例。例如,COM 方法的拦截在几年前就被介绍了。但是,我找不到任何文章或帖子涵盖了本文所介绍的内容。

在本文中,您可以了解到复杂而实用的 COM 组件(例如 tear-off 接口)是如何工作的。此外,您还将熟悉拦截 COM 组件的基本概念。最后,本文还介绍了如何使用调试器找出您的应用程序中发生的情况。

基于该技术实现的软件

正如我在上面部分所述,我开始解决这个问题是为了编写我的应用程序。我正在 我的网站 (http://rodream.net) 上分发使用此技术的应用程序。
具体来说,该应用程序的名称是 'C Browser(See Browser)',它可以禁用一些限制功能的脚本。

参考文献

[1] Volodymyr Shamray, Interception Calls to COM Interfaces
[2] Galen C. Hunt, Michael L. Scott. Intercepting and Instrumenting COM Applications
[3] Zhefu Zhang, COM Interface Hooking and Its Application - Part I
[4] Martin Mueller, Hooking a DirectX/COM Interface
[5] Computerhope.com, Disable mouse right-click
[6] Andrew Whitechapel, ATL Tear-Off Interfaces

历史

  • 2011年12月15日
    • 增加了在 IE8 上的测试结果 (运行良好)
    • 纠正了文章中的拼写和语法错误
  • 2011年12月12日
    • 首次发布
© . All rights reserved.