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,下面自然要来看看 OnBeforeNavigate2 函数了。
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 方法,重新将修改后的数据再提交一次,这样 就实现了数据篡改。具体的代码你们看源码吧。这部分就不详细的说明了。没什么难点