IE表单拦截器
IEC(IE表单拦截器)的实现原理--屌丝也来玩逆袭
在 HTTP 代理实现请求报文的拦截与篡改 (后面简称HTTP 代理 )系列的开篇里,我们提到了一个 IEC ( IE 表单拦截器)的软件,并猜想了他的实现原理是 BHO 或者异步可插入协议,后来想想,完全不靠谱 。同时又因为 VB 的 P-CODE 模式下编译的代码确实太难在汇编层面进行分析,所以就没有再继续去分析它的实现方式,而是另辟蹊径使用了 HTTP 代理来实现了它的功能,这才有了 HTTP 代理 系列。
虽然使用HTTP代理的方式的确是实现了它的功能,但没弄明白它的实现原理,总觉得心理有个事。所以这两天又对其进行了一番分析,虽然VB的PCODE编译的代码反汇编后确实难以分析,但却不代表 反汇编完全没有 作用,找找关键字还是可以的,于是乎祭出各种神器,最终觅得了几个关键字,又百度GOOGLE了一番,最后还是将它的实现原理给还原了出来。 并用wtl实现了它的功能
选用 WTL 而不用 MFC ,是因为 MFC 静态链接后随便编译下就 1M 多的体积让人实在无法忍受。而 WTL 在这方面就表现的很好,编译后 128K ,再 UPX 一下, 60K 不到的体积还是相当让人满意的。另外 WTL 虽然没有 MFC 完善,但处理这种小程序还是绰绰有余的,同时它又是基于 ATL 的,对于 COM 的支持也很好 。不选用大家熟悉的 C# 是因为写这种程序,用 C# 简直就是在找虐。哪位有兴趣的,可以移植一个版本
遵照习惯,讲代码前,先看功能,要想更好的理解代码,至少得知道代码运行后是什么样子的。
解压 附录,根目录下有一个的build文件夹,里面有一个IEIntercepter.exe 。 双击 。
注:WTL 里不知道为什么中文会乱码,懒得解决了,所以这里全部使用的是英文
点击 按钮
如果此时你的 IE 没有打开,则会有如下的提示
OKAY , 打开 IE 。 再次点击
按钮已经变灰了。这说明已经成功的和 IE “绑”在一起了 .
现在我们先不 LOCK ,所以再点下 UNLOCK 解锁,界面变成下面这样
附录根目录下还有一个 testwebsite 文件夹, 这是个 WEBSITE 的工程,我们后面的演示都是基于它下面的 Default.aspx 这个页面的,所以在继续看下面之前,最好把它导入,然后运行 Default.aspx 。当然如果你没有 VS ,也可以参考下面的演示,自己找其它网址进行测试。
运行 Defautl.aspx 运行后界面如下
这时候我们开始 LOCK 。 点一下 按钮
点完 LOCK 后,回到 Default.aspx 页面 , 在 username 框里输入 aaa
点击 submit 。 这时候你会发现我们的程序弹到最前面或者在任务栏闪动图标提示了。看一下此时的程序界面。
从上图可以看出:请求已经给拦截下来了。(第一排虽然写的是GET,但显示的其实是网址,VC里处理字符串实在是太麻烦了,所以直接使用了网址,反正网址?后面的都是GET数据,要想改GET数据,改?后面的就可以了 )
OKAY, 现在我们将GET栏里的 id=1,改成 id=2 ,把POST栏里的username=aaa改成 username=bbb。
然后点击 ( 没有拦截的情况下,此按钮是灰的 ) 。
再回到 Default.aspx 看一下 。
看到什么了 :) 是的,数据已经成功篡改了,又鸡动了一把。
OKAY……功能演示完了,后面就要讲怎么实现了,在这里我们就不象HTTP代理里那样先把程序的总体架构分析一遍然后再逐过程的一句一句的解释了。那样太费时间。这里我们主要讲实现的思路和原理,至于完整的实现,请参考附录的代码
注 : 代码有很多 BUG ,各位自行解决 :)
再注 : 附录的源码是用 WTL 写的, VS 默认是没有 WTL 工程的,需要安装一下。至于如何安装,因为没有固定的说明地址,就不提供地址了,免得到时候链接不在了,直接在 百度 或者 GOOGLE “ VS 安装 WTL ” 一搜一大堆。
再再注: 附录的源码是在VS2010里编译的,其它版本会报平台工具集错误,如果报这个错误,请在解决方案管理器的工程名上右键--属性--配置属性--常规--平台工具集,选择选用的工具集。 VS2012 是V110,VS2010是V100 , VS2008是V90 ...
OKAY,下面我们就开始正式来进行原理讲解 , 它的原理其实并不麻烦 。
首先找到运行中的 IE 的 WebBrowser 控件的窗体句柄,然后利用这个窗体句柄通过 MSAA 技术获得一个已经编排( Marshaling)过的 IWebBrowser2 接口。这个接口的调用和普通的 COM 接口一样,不同的是,他可以在其它进程里调用就象在本地进程中调用一样。说简单点就是,我们在我们的进程里调用这个 IWebBrowser2 接口的相关方法,也就相当于是在刚才获得的那个 IE 里执行相关的方法。 获得这个 IWebBrowser2 后,下一步就是利用这个 IWebBrowser2 的 QueryInterface 方法,获得 IConnectionPointContainer 接口,然后再利用这个接口,查找 DIID_DWebBrowserEvents2连接点 。然后再将一个实现了 DWebBrowserEvents2 接口的类的实例通过这个连接点的 Advise 方法和这个连接点建立起连接。这样,实现了 DWebBrowserEvents2 接口的类的那个实例,就可以接收来自 IE 的事件了。其中当然也包括 BeforeNavigated2 , BeforeNavigated2 有一个 Url 参数,是存地址的,有一个 PostData 参数是来存 POST 数据,还有一个 Cancel 参数,是用来标识是否取消的,如果取消了,那么浏览器就不会将请求继续提交给服务器,如果不取消,就继续提交给服务器,我们要想实现拦截,自然是要把他取消了,然后,再重新包装 Url 和 Post , 再调用 IWebBrowser2 接口的 Navigate2 方法重新将这些请求提交到服务器。
下面上代码
第一步,获得正在运行的 IE 的 WebBrowser 控件的 IWebBrowser2 接口。 MainDlg.h 的 GetIEFromHWnd 方法就是实现这个功能的 。
// 找类名为IEFrame的窗体 hWnd= FindWindow(L " IEFrame " , NULL); // 如果找不到 if (hWnd==NULL || hWnd == 0 ) { // 则找一个类名为CabinetWClass的窗体 hWnd= FindWindow(L " CabinetWClass " , NULL); } // 如果没有找到类名为IEFrame或者CabinetWClass的窗体 if ( hWnd == NULL || hWnd == 0 ){ // 返回NULL return NULL ; } // 然后在hWnd(也就是刚才找到的类名为IEFrame或CabinetWClass的窗体)的子窗体中类名为Shell DocObject View的窗体 // IE6可以直接找到,因为IE6没TAB标签,不过没有测试过 HWND hWndChild = FindWindowEx(hWnd, 0 , L " Shell DocObject View " , NULL); // 如果没有找到,说明是IE6以上 if (hWndChild == 0 ){ // 就在hWnd的子体中找类名为Frame Tab的窗体 hWndChild = FindWindowEx(hWnd, 0 , L " Frame Tab " , NULL); if (hWndChild == 0 ){ return NULL; } // 然后继续在类名为Frame Tab的窗体的子窗体中找类名为TabWindowClass的窗体 hWndChild = FindWindowEx(hWndChild, 0 , L " TabWindowClass " , NULL); if (hWndChild == 0 ){ return NULL; } // 然后继续在类名为TabWindowClass的窗体的子窗体中找类名为Shell DocObject View的窗体 hWndChild = FindWindowEx(hWndChild, 0 , L " Shell DocObject View " , NULL); if (hWndChild == 0 ){ return NULL; } } // 在类名为Shell DocObject View的窗体的子窗体中找类名为Internet Explorer_Server的窗体 // 这个就是WebBrowser 控件的窗体了 hWndChild = FindWindowEx(hWndChild, 0 , L " Internet Explorer_Server " , NULL); if (hWndChild== 0 ){ return NULL; } // 将WebBrowser控件的窗体句柄赋给hWnd hWnd= hWndChild; // 我们需要显示地装载OLEACC.DLL,这样我们才知道有没有安装MSAA(Microsoft Active Accessibility) // 因为要跨进程的操作,所以是必须的 , 他的 ObjectFromLresult 是获得 接口的关键 HINSTANCE hInst = LoadLibrary( _T( " OLEACC.DLL " ) ); CComPtr <IWebBrowser2> pWebBrowser2 ; // 如果hInst不为NULL,也就是支持MSAA 。 if ( hInst != NULL ){ // 如果找到了运行中的IE的WebBrowser控件的窗体句柄 if ( hWnd != NULL ){ LRESULT lRes; // 注册一个WM_HTML_GETOBJECT的消息 UINT nMsg = ::RegisterWindowMessage( _T( " WM_HTML_GETOBJECT " ) ); // 象WebBrowser控件的窗体发送 WM_HTML_GETOBJECT 并将返回结果,存放在lRes变量里 ::SendMessageTimeout( hWnd, nMsg, 0L , 0L , SMTO_ABORTIFHUNG, 1000 , (DWORD*)& lRes ); // 得到OLEACC.DLL 里 ObjectFromLresult 方法的 地址 LPFNOBJECTFROMLRESULT pfObjectFromLresult = (LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst, (LPCSTR) " ObjectFromLresult " ); // 如果找到了 ObjectFromLresult 方法 if ( pfObjectFromLresult != NULL ){ HRESULT hr; // 声明一个IHTMLDocument2*的实例. CComPtr<IHTMLDocument2> spDoc; // 利用ObjectFromLresult的通过 WM_HTML_GETOBJECT 的返回值,得到WebBrowser的IHTMLDocument2 hr = pfObjectFromLresult(lRes,IID_IHTMLDocument2, 0 ,( void **)& spDoc); // 如果执行成功 if ( SUCCEEDED(hr) ){ // 声明一个IHTMLWindow2*和一个IServiceProvider*实例 。 CComPtr<IHTMLWindow2> spWnd2; CComPtr <IServiceProvider> spServiceProv; // 通过 IHTMLDocument2 的 get_parentWindow 得到 IHTMLWindow2 接口 hr=spDoc->get_parentWindow ((IHTMLWindow2**)& spWnd2); // 如果成功 if (SUCCEEDED(hr)){ // 通过IHTMLWindow2的QueryInterface方法,获取IServiceProvider 接口 hr=spWnd2->QueryInterface (IID_IServiceProvider,( void **)& spServiceProv) ; // 如果成功 if (SUCCEEDED(hr)){ // 利用 IServiceProvider 接口的 QueryService 方法 获取 IWebBrowser2 接口,至此这个接口就算找到了 hr = spServiceProv-> QueryService(SID_SWebBrowserApp,IID_IWebBrowser2,( void **)& pWebBrowser2); } } } } } ::FreeLibrary(hInst); } else { // 如果没有安装MSAA // MessageBox(NULL,_T("Please Install Microsoft Active Accessibility"),"Error",MB_OK); }
这些代码没什么讲头,注释已经非常详细了。
至于为什么找窗体要这么曲折,看一下下面的图你就明白了。
获得 IWebBrowser2 接口后,下一步就是获取连接点,然后将实现了 DWebBrowserEvents2 接口的类的实例和这个连接点连接起来。连接点是 COM 里的概念,可以类比成事件。
MainDlg.h 里的 RegisterSelfAsIeEventDealer就是实现这个功能的。
void RegisterSelfAsIeEventDealer(IWebBrowser2 * ppWebBrowser2) { // 声明一个IConnectionPointContainer和IConnectionPoint实例。 CComPtr<IConnectionPointContainer> spConnectionPointContainer; CComPtr <IConnectionPoint> spConnectionPoint; // pWebBrowser2->QueryInterface(IID_IConnectionPointContainer,(void**)&spConnectionPointContainer); // 利用 IWebBrowser2 接口的 QueryInterface 方法获得 IConnectionPointContainer 接口 ppWebBrowser2->QueryInterface(IID_IConnectionPointContainer,( void **)& spConnectionPointContainer); // 利用 IConnectionPointContainer 接口的 FindConnectionPoint 获取 IID为DIID_DWebBrowserEvents2 的连接点 spConnectionPointContainer->FindConnectionPoint(DIID_DWebBrowserEvents2,& spConnectionPoint); // 利用IID为DIID_DWebBrowserEvents2的连接点的Advise建立一个实现了DWebBrowserEvents2接口的接收器的实例和此连接点的连接。 // 第一个参数就是接收器的实例,必须是一个实现了DWebBrowserEvents2接口的类的实例 // 在这里我们设置成this,也就是自己实现了DWebBrowserEvents2接口,这个是通过继承CWebEventSink实现的 spConnectionPoint->Advise( this ,& m_dwCookie); }
DWebBrowserEvents2 接口其实就是一个 IDispatch 接口。因为要实现的方法比较多,为了不使 CMainDlg 类看起来太混乱,所以我们在 WebEventSink.h和WebEventSink.cpp 这两个文件里实现了一个实现了 DWebBrowserEvents2 接口的类 CWebEventSink . 然后 CMainDlg : CWebEventSink 。这样 CMainDlg 也就实现了 DWebBrowserEvents2 接口 。这就是为什么 RegisterSelfAsIeEventDealer 方法的最后一句的第一个参数传递的是 this 的原因了,当然 DWebBrowserEvents2 接口的具体实现还是在 CWebEventSink 类里的,这些实现中,其它的都不讲了,只有一个是最重要的。
// This is called by IE to notify us of events // Full documentation about all the events supported by DWebBrowserEvents2 can be found at // http://msdn.microsoft.com/en-us/library/aa768283 (VS.85).aspx STDMETHODIMP CWebEventSink::Invoke(DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS *pDispParams,VARIANT *pVarResult,EXCEPINFO *pExcepInfo,UINT * puArgErr) { UNREFERENCED_PARAMETER(lcid); UNREFERENCED_PARAMETER(wFlags); UNREFERENCED_PARAMETER(pVarResult); UNREFERENCED_PARAMETER(pExcepInfo); UNREFERENCED_PARAMETER(puArgErr); if (!IsEqualIID(riid,IID_NULL)) return DISP_E_UNKNOWNINTERFACE; // riid should always be IID_NULL switch (dispIdMember) { case DISPID_BEFORENAVIGATE2: OnBeforeNavigate2( (IDispatch *)pDispParams->rgvarg[ 6 ].byref, (VARIANT *)pDispParams->rgvarg[ 5 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 4 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 3 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 2 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 1 ].pvarVal, (VARIANT_BOOL *)pDispParams->rgvarg[ 0 ].pboolVal ); break ; } return S_OK; }
前面提到过连接点就类似事件,现在我们把 CMainDlg 的实例作为接收器和 IID 为 DIID_DWebBrowserEvents2 的连接点建立了连接,那么每当浏览器需要触发一些事件的时候,他就会调用和 IID 为 DIID_DWebBrowserEvents2 的连接点建立了连接的接收器的 Invoke 方法。 Invoke 方法的第一个参数 dispIdMember 就是要调用的事件处理方法的 ID (标识符), pDispParams 则存储着调用事件处理方法 所需要的参数 。
那么这时候 IE 如果要触发一个 BeforeNavigated2 事件,它会怎么做呢,对头,会调用我们的 Invoke 方法,然后第一个参数会传递一个 DISPID_BEFORENAVIGATE2 。并在 pDispParams 里把需要的参数全部传递过来 。
那么IE调用了我们的 Invoke 方法,并传递了如上所描述的参数后,我们这边又会做些什么呢。看看代码就明白了 。
switch (dispIdMember) { case DISPID_BEFORENAVIGATE2: OnBeforeNavigate2( (IDispatch *)pDispParams->rgvarg[ 6 ].byref, (VARIANT *)pDispParams->rgvarg[ 5 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 4 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 3 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 2 ].pvarVal, (VARIANT *)pDispParams->rgvarg[ 1 ].pvarVal, (VARIANT_BOOL *)pDispParams->rgvarg[ 0 ].pboolVal ); break ;
}
是的,当IE调用我们的Invoke后,我们会继续将调用自己实现的 OnBeforeNavigate2 方法。
OKAY ,现在再我们把刚才的连起来总结一遍。执行完 RegisterSelfAsIeEventDealer 方法将自己作为接收器和 DIID_DWebBrowserEvents2 连接点建立起连接后,每当 IE 要触发 BeforeNavigate2 事件的时候,都是去调用我们的 OnBeforeNavigate2 方法,这也就相当于我们的 OnBeforeNavigate2 的方法就是 IE 的 BeforeNavigate2 事件的处理函数。你现在完全可以把我们的这个 OnBeforeNavigate2 方法,类比成你在 C# 窗体里拖一个 WebBrowser 控件,然后在这个控件的事件窗口里选择 OnBeforeNavigate2 事件,双击后 VS 帮你 自动 建的那个 WebBrowser控件名_OnBeforeNavigate2 方法。只是你建的那个方法,处理的是你拖的那个 WebBrowser 控件的 OnBeforeNavigate2 。而我们这个处理的是 IE 的 OnBeforeNavigated2 事件 。
OKAY, 下面自然要来看看 O nBeforeNavigate2 函数了。
OnbeforeNavigate2 的 定义在 WebEventSink.h 里
virtual STDMETHODIMP OnBeforeNavigate2( IDispatch *pDisp, VARIANT *pvUrl, VARIANT *pvFlags, VARIANT * pvTargetFrameName, VARIANT *pvPostData, VARIANT *pvHeaders, VARIANT_BOOL *pvCancel) { return S_OK; }
我们把他定义成了一个虚函数。 具体的实现在 CWebEventSink 的子类 CMainDlg 里。 也就是在 MainDlg.h 这个文件里
OnbeforeNavigate2 有个很重要的参数 VARIANT_BOOL *pvCancel 也就是最后一个参数。如果在 OnbeforeNavigate2 方法体内 *pvCancel = VARIANT_TRUE ;
STDMETHODIMP OnBeforeNavigate2( IDispatch *pDisp, VARIANT *pvUrl, VARIANT *pvFlags, VARIANT * pvTargetFrameName, VARIANT *pvPostData, VARIANT *pvHeaders, VARIANT_BOOL * pvCancel) { // 取消继续提交。 *pvCancel = VARIANT_TRUE ; }
这样 IE 就不会将这个请求继续提交到服务器,如果 *pvCancel = VARIANT_FALSE ; 则会继续提交。
这就给我们篡改数据,提供了便利。如果我们想篡改数据,只要在这个方法体内,将所有参数保存一份,然后, *pvCancel = VARIANT_TRUE ; 这样 IE 就会取消这次请求,这时候就可以更改数据了,主要是更改 pvUrl 和 pvPostData 两个数据。 pvUrl 存储是要提交的网址,其中 ? 后面的就是 GET 部分, pvPostData 存储的就是 POST 的数据。 改完数据后,再利用 IE 的 IWebBrowser2 接口 的 Navigate2 方法,重新将修改后的数据再提交一次,这样 就实现了数据篡改。具体的代码你们看源码吧。这部分就不详细的说明了。没什么难点
附录(程序+源码)
作者: Leo_wl
出处: http://www.cnblogs.com/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息