Browser Helper Object

那天有个信息安全实验室的博士生叫我们做IE插件,主要用于拦截IE浏览器的各项消息,从而做出我们自己的处理。

实现方法,当然千奇百怪,全局钩子函数,进程注入,消息拦截等等都可以,不过我最终选择了全局钩子函数,感觉应该好做一点。

由于最近比较多接触C#,第一反映当然还是.net framework。
MSDN->Filter:c#->“全局钩子”一找,郁闷死人:

在 .NET 框架中不支持全局挂钩

您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。
[http://support.microsoft.com/kb/318804]

当然,其实这段文字本身有点问题,先不说网上有人说好像并不是所有的全局挂钩都不能加载,但就.net支持对非受控代码的访问这点,就可以通过在中间增加一层Managed Code->UnManaged Code->Managed Code实现调用。

但是这样的实现难度陡增,很是麻烦。不过在搜索结果中看到一个比较乖的咚咚:Browser Helper Object,看起来感觉比较象。

记得以前有篇写Office Doc Object架构的文章很有名(好像小排发过吧),不过记忆中,当时看了一点就放弃了,感觉挺麻烦的。特别是针对COM组件的调用部分,看着就头大。

话说回来,调用COM组件在.net环境下,还是C++来得方便快捷,毕竟都是非受控的代码,不用处理太多受控、非受控对象之间的交互问题。不过C++.net依然还是VC++6.0的模样,看着都恶心,于是乎放弃:P

粗粗看了看Browser Helper Object(BHO)的机理,这个咚咚主要用于下面的场景:

想实现一个浏览器,提供一些自己的功能,但是如果用IE Control来写的话,还要自己写很多很多很多诸如:前进、后退、历史记录等等IE已经提供的功能,所以更简便的方法是给扩展、修改IE的功能,直接使用IE来实现。这方面,当然不能不提很多人深恶痛绝的3721,他提供了一个个性化的浏览器,却只写了很少的代码,其余的都是IE自带的内容。

打开一个IE,打开Spy++,再结合Internet Explorer 4.0 Architecture的图看看

Internet Explorer 4.0 Architecture

举个例子,如果想要获取IE地址栏的内容:
IEExplorer->IEFrame->WorkerW->ReBarWindow32->ComboBoxEx32->ComboBox->Edit
这就是他的层次结构

IE和Explorer在启动新线程的时候(Explorer要看看选项里面是否选择了在新的线程中打开窗口),会检查注册表项:

[HKLMSOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects]

他下面有很多子项,每一个子项就是一个GUID
例如:
{06849E9F-C8D7-4D59-B87D-784B7D6BE0B3}

这个GUID对应一个BHO组件(其实就是一个COM组件)
这个组件在[HKLMSOFTWAREClassesCLSID]下面进行描述,包括dll路径,签名等等信息

IE或Explorer在每个线程里会依次实例化每个在上述路径注册的BHO对象,从而BHO对象实际上运行在IE或Explorer的地址空间内,并且能够访问IE的几乎所有资源,拦截几乎所有事件,挂载自己的事件处理函数,而不需要跨进程调用和传消息。

看来完全符合最初的应用要求并且是最简单的方法了。

接下来的问题,就只剩下COM组件的使用了。.NET对COM组件的调用提供了很方便的100% pure 受控的访问方式 – Interop, Marshal。查错过程比较艰辛,因为每次重新生成都要重启Explorer.exe T T并且调试的时候,要先开IE,再下断点,再将VS .NET附加到IE进程,再点新建IE窗口…

COM组件的使用:
首先,当然是用Create GUID工具生成我们自己BHO组件的GUID({B29E305D-BC4D-4a80-B522-B0ABC9EBDFFC}),然后添加对 Microsoft Internet Controls COM组件(%systemroot%ShDocVw.dll)的引用。VS.NET会自动Wrapper之为 Interop.SHDocVw.dll,值得注意的是,这里由于最后要使用BHO注册,所以要求这个COM组件必须用私钥签名,从而使用强名机制。具体步骤如下:
1、sn -k BHO.key生成Bho.key文件,也就是我们的私钥。
2、设置项目属性,ActiveX/COM对象的包装程序集->包装程序集密钥文件,指向Bho.key
3、添加COM引用(此时查看相关信息就可以看到强名选项已设为true)
4、设置AssemblyInfo.cs填写key字段,指向Bho.key

然后,就可以声明我们要继承的各项接口:

using SHDocVw;
#region 引入IObjectWithSite接口
[ComImport(), Guid("fc4801a3-2ba9-11cf-a229-00aa003d7352")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IObjectWithSite
{
void SetSite([In ,MarshalAs(UnmanagedType.IUnknown)] object site);
void GetSite(ref Guid guid, [MarshalAs(UnmanagedType.IUnknown)] out object site);
}
#endregion
#region 定义默认BHO COM接口
[GuidAttribute("181C179B-7CC9-4457-8C1D-4B45E7C8589D")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)]
public interface IObserver
{
}
#endregion

然后定义我们自己的BHO类:

/// <summary>
///定义BHO类,此类由浏览器实例化
/// </summary>
[ClassInterfaceAttribute(ClassInterfaceType.None)]
[GuidAttribute("B29E305D-BC4D-4a80-B522-B0ABC9EBDFFC")]//由CreateGUID程序生成
[ProgIdAttribute("Observer.BrowserMonitor")]
public class BrowserMonitor : IObserver, IObjectWithSite
{}

这个类里面如下引入定义:

protected IWebBrowser2 browser; //浏览器对象
protected DWebBrowserEvents2_Event browserEvents;//浏览器事件

并且实现IObjectWithSite接口:

#region IObjectWithSite 成员
/// <summary>
/// IE调用此方法,并传递指向容器Site的IUnknown指针,由此我们可以获得IWebBrowser2接口
/// 并挂载DWebBrowserEvents2事件
/// </summary>
/// <param name="site">容器Site的IUnknown指针</param>
public void SetSite(object site)
{
// TODO: 添加 BrowserMonitor.SetSite 实现
#region 取得 IWebBrowser2 引用
if (browser != null)
Release();
if (site == null)
return;
browser = site as IWebBrowser2;
#endregion
#region 检查名称,当且仅当为IEXPLORE.EXE时加载
string hostName = browser.FullName;
if (!(hostName.ToUpper().EndsWith("IEXPLORE.EXE")))
{
Release();
return;
}
#endregion
#region 挂载浏览器事件
browserEvents = browser as DWebBrowserEvents2_Event ;
if (browserEvents != null)
{
browserEvents.DocumentComplete += new DWebBrowserEvents2_DocumentCompleteEventHandler(this.OnDocumentComplete);
}
else
{
Release();
return;
}
#endregion
}
/// <summary>
/// 调用者调用此方法以获得前面浏览器发送给SetSite()方法的浏览器对象
/// </summary>
/// <param name="guid">请求Site接口对象的GUID</param>
/// <param name="site">返回的Site接口对象</param>
public void GetSite(ref System.Guid guid, out object site)
{
// TODO: 添加 BrowserMonitor.GetSite 实现
site = null;
if (browser != null)
{
IntPtr pSite = IntPtr.Zero;
IntPtr pUnk = Marshal.GetIUnknownForObject(browser); //引用计数增加
Marshal.QueryInterface(pUnk, ref guid, out pSite); //引用计数增加
Marshal.Release(pUnk); //引用计数减少
Marshal.Release(pUnk); //引用计数减少
if (!pSite.Equals(IntPtr.Zero))
{
site = pSite;
}
else
{
// 若找不到请求的接口,将返回E_NOINTERFACE
Release();
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
}
}
else
{
// 若找不到请求的接口对象,将返回E_FAIL
Release();
Marshal.ThrowExceptionForHR(E_FAIL);
}
}
#endregion

这里挂载了我们自己的事件处理函数,处理DocumentComplete事件,此外还有很多事件可以使用,例如:

DownloadBegin
DownloadComplete
BeforeNavigate2
CommandStateChange
FileDownload
NavigateComplete
NewWindow
FullScreen
MenuBar
Quit
FrameBeforeNavigate
FrameNavigateComplete
FrameNewWindow
ProgressChange
PropertyChange
StatusTextChange
TitleChange
WindowActivate
WindowMove
WindowResize

using System.IO;
#region 用于挂载的自定义函数
protected void OnDocumentComplete(object display, ref object url)
{
try
{
if (Marshal.Equals(browser, display))
{
StreamWriter sw = new StreamWriter(@"C:Test.txt", true);
sw.WriteLine(url.ToString());
sw.Close();
}
}
catch
{
Release();
Marshal.ThrowExceptionForHR(E_FAIL);
}
}
#endregion

此外,关于错误处理,由于涉及包裹为受控组件的非受控组件,所以需要使用以下方式抛出异常:

#region 定义HRESULT值 : 预定义的COMException值
//在未检查的上下文中,如果表达式产生目标类型范围之外的值,则结果被截断
const int E_FAIL = unchecked((int)0x80004005);//失败
const int E_NOINTERFACE = unchecked((int)0x80004002);//QueryInterface时,接口不存在
#endregion
Marshal.ThrowExceptionForHR(E_FAIL);
Marshal.ThrowExceptionForHR(E_NOINTERFACE);

使用一下方式,释放对象:

#region Release操作
protected void Release()
{
if (browserEvents != null)
{
Marshal.ReleaseComObject(browserEvents);
browserEvents = null;
}
if (browser != null)
{
Marshal.ReleaseComObject(browser);
browser = null;
}
}
#endregion

至此已基本完成,但是要想自动注册BHO组件,还需要做几件事情:

#region 挂载注册/注销操作
/// <summary>
/// 在注册COM组件时,由运行时调用
/// </summary>
[ComRegisterFunctionAttribute]
public static void Register(Type type)
{
// 注册BHO组件
string guid = type.GUID.ToString("B");
RegistryKey rkey =
Registry.LocalMachine.CreateSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects");
RegistryKey rkeyBHO = rkey.CreateSubKey(guid);
}
/// <summary>
/// 在注销COM组件时,由运行时调用
/// </summary>
[ComUnregisterFunctionAttribute]
public static void Unregister(Type type)
{
// 注销BHO组件
string guid = type.GUID.ToString("B");
RegistryKey rkey =
Registry.LocalMachine.CreateSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects");
rkey.DeleteSubKey(guid,false);
}
#endregion

由这些可见.NET平台对于COM组件的调用支持还是相当强大的,可以在受控环境下实现100% pure的COM访问能力。

PS:
结尾怎么写成这样了,写一写的就有点偏题了- –

BHO Project

《Browser Helper Object》有10个想法

  1. 不用这样吧,你完全可以在.net环境下调用win32 API来创建钩子。
    但我不确定你是否能回调成功.net的函数

  2. 主要原因是在longhorn没有推出前,这种小程序用.net会很让使用者郁闷…

  3. >>我不确定你是否能回调成功.net的函数
    MSDN说的,托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。我感觉应该全局钩子不用非托管层没法完成回调吧。
    >>主要原因是在longhorn没有推出前,这种小程序用.net会很让使用者郁闷…
    这个就不管了,给那个怨念的博士交差而已,其实感觉上用ATL对象调COM对象还要简单一点
    🙂

  4. 那个意思似乎是delegate的机制已经跟函数指针不大一样了.
    另, 好像有个向导自动生成COM的wrapper类.

  5. >>好像有个向导自动生成COM的wrapper类
    恩,其实是用到了的,就是CCW

回复 zengxi 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据