在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 方法,重新將修改后的數據再提交一次,這樣 就實現了數據篡改。具體的代碼你們看源碼吧。這部分就不詳細的說明了。沒什么難點