環境:VC6/VC7, MS Platform Core SDK, IE4.0+, WinNT/2K/XP (在英文/中文/日文版的Win2k/XP 及IE6.0+SP1上測試通過)
關鍵字:Windows 鉤子,IE COM 對象,Win2k 安全上下文 IE編程 工具 系統
摘要
本文將介紹一個工具,它不僅能偷窺各種桌面程序的密碼框,還能窺到IE頁面中的密碼框,這個程序就是本文要介紹的——SuperPasswordSpy++。
使用 Windows 鉤子偷窺遠程進程(或者說桌面程序)密碼框內容不是太難,但要偷窺到網頁上密碼輸入域的內容要如何做呢?顯然,在網頁裡的密碼輸入框不是一個窗口,你得借助 IHTMLDocument2 接口來枚舉並吸取密碼。本文提供的工具程序將向你展示破解密碼編輯公共控件和IE密碼輸入域的內容。下圖是程序運行的截圖:
圖一:SuperPasswordSpy++ 偷窺Hotmail 頁面上的密碼輸入框
圖二 SuperPasswordSpy++ 偷窺IIS5.0和WinXP密碼編輯框內容
程序架構
在你圍繞屏幕拖動放大鏡,程序便捕獲鼠標位置並跟蹤鼠標的移動,只要鼠標移到某個新窗口,程序便檢查該窗口的類名以及窗口式樣,確定窗口是否為密碼編輯框或IE(IE實際上是一個浏覽器控件,為方便起見叫IE)。如果是IE,那麼鉤子DLL必須被立即注入到IE中,以確定IE是否包含密碼輸入域。
我們有兩種可選的方法來實現鉤子:
第一種方法是設置 WH_GETMESSAGE 鉤子,可以參考 Brian Friesen 的文章和代碼——PasswordSpy(http://www.codeguru.com/samples/pwdspy.html)。該鉤子DLL對消息進行截獲並應用同步對象(互斥,事件等)。如圖三:
圖三Spy 程序鉤子的一般架構
上圖涉及到五個步驟:
Spy程序將鉤子DLL注入到目標程序;
Spy程序發送(post)一個用戶消息到目標程序,該消息將被注入的DLL截獲;
DLL截獲用戶消息並讀取密碼編輯框內容;
DLL將數據發回到Spy程序;
Spy程序獲取數據並卸載鉤子DLL;
因為Spy程序發送用戶消息,第二步不會“阻塞”,Spy程序不知道第四步何時發生。通常這種Spy程序使用WM_COPYDATA來傳輸字節數據,即 “發送”消息。此處不足之處是當用戶來回移動放大鏡時可能會有這樣的情況——Spy程序發現密碼編輯框,注入DLL到目標程序,發送用戶消息,這時用戶突然移動放大鏡到另一個目標程序的密碼輸入框,此時Spy程序不得不從老的目標程序中卸載DLL鉤子,鉤子進入新的目標程序並發送消息。不幸的是,來自老的目標程序的WM_COPYDATA 消息已經進入目標程序的消息隊列,如果你不將目標窗口句柄信息添加到WM_COPYDATA,,你便無法告知當前目標程序的數據。
第二種方法是設置WH_CALLWNDPROC鉤子。這種方法與第一種方法類似,只是第二步發送消息到目標程序的方法有所不同,這裡不是POST,而是 SEND。鉤子DLL將截獲用戶消息並進行密碼的內部讀取,使用WM_COPYDATA調用SendMessage將數據傳到Spy程序,總之,第二步被阻塞,直到第三步,第四步完成,所以第二步完成後,我們可以直接做第五步。這樣一來,代碼將比第一種方法簡單多了。它類似於Block Socket和非Block Socket編程。
但是,第二種方法也有其弊端。考慮下面的情況,假設相同的窗口中有兩個密碼編輯框,用戶同時只能顧一個密碼框,第一種方法,我們可以檢查兩個密碼框屬於同一個線程,鉤子DLL只運作一次。而第二種方法,鉤子要運作兩次,這造成一定的開銷(本文本程序可以忽略這種開銷)。
實現細節描述
1、如何從浏覽器控件窗口句柄獲取IHTMLDocument(參見MSDN KB Q249232——HOWTO: 如何從從HWND獲取IHTMLDocument2)
BOOL HWnd2HtmlDocument()
{
CoUninitialize();
HINSTANCE hInst = ::LoadLibrary( _T("OLEACC.DLL") );
if ( hInst == NULL ) return FALSE;
LRESULT lRes;
UINT nMsg = ::RegisterWindowMessage( _T("WM_HTML_GETOBJECT") );
::SendMessageTimeout( g_hTarget, nMsg,
0L, 0L, SMTO_ABORTIFHUNG, 1000, (DWORD*)&lRes );
LPFNOBJECTFROMLRESULT pfObjectFromLresult =
(LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst,
"ObjectFromLresult");
if ( pfObjectFromLresult == NULL )
{
::FreeLibrary( hInst );
CoUninitialize();
return FALSE;
}
WCHAR strDoc[] = L"{626fc520-a41e-11cf-a731-00a0c9082637}";
//IID_IHTMLDocument2 CLSID
CLSID uuidDoc;
HRESULT hrDoc = CLSIDFromString((LPOLESTR)strDoc,
&uuidDoc //IID_IHTMLDocument2
);
if(!SUCCEEDED(hrDoc))
{
::FreeLibrary( hInst );
CoUninitialize();
return FALSE;
}
HRESULT hr = (*pfObjectFromLresult)( lRes, uuidDoc,
//IID_IHTMLDocument,
0, (void**)&g_lpHTMLDocument2);
if ( SUCCEEDED(hr) )
{
//OK, We Get Here Successfully
}
else
{
::FreeLibrary( hInst );
CoUninitialize();
return FALSE;
}
::FreeLibrary( hInst );
CoUninitialize();
return TRUE;
}
在此我需要解釋 g_hTarget 是浏覽器句柄,其類名是“Internet Explorer_Server”。通常,如果使用MS IE浏覽器,不會出現問題,但某些應用使用 Web 浏覽器的ActiveX 控件,在浏覽器導航之前,“Internet Explorer_Server”窗口是不存在的。如圖我們來看一個例子:
圖四 Spy++截圖:一個對話框有兩個MS Web Browser ActiveXs
圖四是運行Spy++的一張截圖,該對話框(00060294)有兩個浏覽器,只有浏覽器000202D6導航到某些URL。你可以做個實驗,在對話框中放一個WebBrowser ActiveX,用 Spy++看看結果。
2、當前頁面是否包含密碼輸入域
DWORD CheckHtmlDocument()
//返回: 0 —— 無密碼輸入,否則 —— 有密碼輸入
{
MSHTML::IHTMLElementCollection *pForm;
HRESULT hr = g_lpHTMLDocument2->get_all(&pForm);
//g_lpHTMLDocument2 是一個IHTMLDocument2 指針
if(FAILED(hr)) return 0;
long len;
pForm->get_length(&len); //How many elements on this form?
DWORD dwRet = 0;
for(int i = 0; i < len; i++)
{
LPDISPATCH lpItem = pForm->item(CComVariant(i),
CComVariant(i));
MSHTML::IHTMLInputElementPtr lpInput;
HRESULT hr = lpItem->QueryInterface(&lpInput);
//它是輸入域嗎?
if(FAILED(hr)) continue;
_bstr_t type(_T("password"));
if(lpInput->Gettype() == type) //Check Field Type
{
//_bstr_t x = lpInput->Getvalue();
//If you want its string
dwRet++;
}
lpItem->Release(); //記住釋放!
lpItem = NULL;
}
pForm->Release();
pForm = NULL;
return dwRet;
}
3、從當前頁面密碼輸入域吸取密碼
_bstr_t x = lpInput->Getvalue(); //And you go!
LPCTSTR lpWhatEver = (LPCTSTR)x;
//在這裡對密碼進行處理
SuperPasswordSpy++ 程序說明——該程序基於Unicode,需要在WinNT/2K/XP + IE4.0+中運行。
讀者可以通過本文附帶的程序代碼了解如何跟蹤窗口和遠程進程鉤子。同時極力推薦讀者研究 MS Platform SDK 的例子程序 SPY 和 Brian Friesen 先生的 PasswordSpy。筆者不喜歡“重新發明輪子”之類的事情,本文涉及的代碼借鑒了 SPY 例子(如鼠標跟蹤)以及PasswordSpy(如函數 SmallestWindowFromPoint和資源)的實現。如果讀者有關於鉤子技術的疑問,請閱讀Brian Friesen 的相關文章,他的文章在這方面講很詳細。此外有關DLL中共享區的詳細實現方法建議閱讀 Jeffrey Richter 的“Programming Application for MS Windows”第四版。
下面是本文程序SuperPasswordSpy++ 實現中的其它一些細節:
1、判斷密碼編輯框的條件
BOOL IsPasswordEdit(HWND hWnd)
{
TCHAR szClassName[64];
int nRet = GetClassName(hWnd, szClassName, 64);
if(nRet == 0) return FALSE;
szClassName[nRet] = 0;
if(::lstrcmp(szClassName, _T("Edit")) != 0 &&
::lstrcmp(szClassName, _T("TEdit")) != 0
&&| ::lstrcmp(szClassName, _T("ThunderTextBox"))
!= 0 ) return FALSE;
//Here, is it OK?
DWORD dw = ::GetWindowLong(hWnd,GWL_STYLE);
dw &= ES_PASSWORD;
if(dw == ES_PASSWORD)
return TRUE;
return FALSE;
}
以上代碼是SuperPasswordSpy++ 實現,要注意一點:密碼編輯框的判斷是根據類名實現的,筆者在代碼中的判斷方法是檢查類名是否為“TEDIT," "IRIS.PASSWORD” 或者“"EDIT.”。這是Borland的命名規范:TxxxClass。“ThunderTextBox”是由Visual Basic創建得類名。我無法保證今後Windows系統中的應用程序密碼編輯框的類名不會改變。誰知道Visual Studio今後的版本中密碼編輯框會叫什麼。如果不幸碰到這樣的情況,請大家自行修改SuperPasswordSpy++的相關代碼。
2、當HTML頁面文件中存在多個 Frame 標簽時
SuperPasswordSpy++目前的版本假設在HTML頁面文件如果包含密碼輸入域,它只有一個 Frame。對於大多數有密碼輸入域的頁面來說(如MSN Hotmail),SuperPasswordSpy++都能正常運行。但為了做得更完美些,我會在下個版本的SuperPasswordSpy++中添加對多個 Frame 的支持。請隨時到我的站點檢查更新。
其它說明
1、Windows 登陸密碼:(僅適用於Win2K )
似乎有些瘋狂,但是看看如下圖片:
圖五
圖五 SuperPasswordSpy++ 窺視Win2K 服務器的“更改密碼”的編輯框,用“Ctrl+Alt-Del”就可以看到“更改密碼”按鈕。PasswordSpy++在Win2K的登陸桌面的 SYSTEM上下文中已經啟動,這時可以讀取到“更改密碼”框中的秘密。在Windows2003系統中這個方法就不靈了。
為了完成這個實驗,你得借助工具在Windows的登陸桌面(Winlogon Desktop)來啟動程序。你可以到 codeguru 獲取“GUI-RunAs” 程序。選擇Winlogon桌面,不要輸入用戶名,這樣就可以使用“SYSTEM”身份,記住你必須具有 Administrator 權限來完成這個工作。按照文章中的說明,你可能需要注銷當前會話一次(只要一次)以便啟用某些還不具備的權限。有些讀者來信說Windows系統已經有一個“RunAs”命令行程序了。我當然知道,但是微軟品牌的這個“RunAs”命令行工具無法選擇“桌面”來啟動程序,而我的工具能做到。如果你在使用這個工具時有什麼問題(例如,啟動的程序GUI失敗),請先用“SYSTEM”身份啟動這個工具,然後再啟動你使用的程序。
最後,為了獲得屏幕截圖,出現WinLogon桌面時按“Print Screen”,回到默認桌面,將它粘貼到MSPaint程序。你還可以用 RunAs工具在WinLogon屏幕啟動MSPaint,不用來回切換屏幕。你還會發現這個 RunAs工具另一個好處,以“SYSTEM”身份啟動任務管理器,殺死一些頑固的進程(包括Windows服務)。
2、編輯框的偷窺與反偷窺
有人如何反偷窺。其實不需要花費太大的功夫就可以實現反偷窺。下面的代碼例子使用MFC,首先派生一個 CEdit,我最初想改寫PreTranslate函數,就像下面這樣:
BOOL CAntiPeekEdit::PreTranslateMessage(MSG* pMsg)
可惜 if 語句中的代碼不會被調用,為什麼呢?只有MFC 開發團隊知道。沒辦法我把注意力轉到函數 WindowsProc,這樣就可以行得通了。
//這個方法不靈!
{
if(pMsg->message == WM_GETTEXT)
{
//Only Report Text When Passing a Fixed Length Buffer;
if(pMsg->wParam == 1024) //The Number Only You Know
{
}
else
{
::lstrcpy((LPTSTR)(pMsg->lParam), _T("Nothing"));
return TRUE;
}
}
return CEdit::PreTranslateMessage(pMsg);
}
LRESULT CAntiPeekEdit::WindowProc( UINT message,
WPARAM wParam,
LPARAM lParam)
//This works!
{
if(message == WM_GETTEXT)
{
if(wParam == 1024) //The Number Only You Know
{
return CEdit::WindowProc(message, wParam, lParam);
}
else
{
::lstrcpy((LPTSTR)(lParam), _T("Nothing"));
//Insert Dummy Text Here To the Peeker
return 7;
}
}
return CEdit::WindowProc(message, wParam, lParam);
}
在你自己的程序中,如果想要獲得密碼框的文本,你得像下面這樣做:
TCHAR sz[1024];
::SendMessage(hPasswordEdit, WM_GETTEXT, 1024, (LPARAM)sz);
如果其它例程調用以獲取秘密,由WM_GETTEXTLENGTH 會得到正確的長度,但是當正確的長度緩沖給了我們的 AntiPeekEdit,我們便知道它由某些其它的非安全源代碼調用,所以我們可以返回垃圾數據。你還可以完全放棄 WM_GETTEXT 並使用 WM_USER + 123 消息來獲取文本。讓我們回頭來考慮如何化解這種AntiPeekEdit。我們知道為何要使用鉤子DLL,同時在遠程進程中查詢密碼,Win2K密碼編輯框是不接受來自本進程邊界以外的 WM_GETTEXT 消息的。以上策略以用戶定義的過程代替標准的Edit類窗口過程,那麼,反過來當 SuperPasswordSpy++ 進行偷窺活動時,如何用標准的Edit類窗口過程代替用戶定義的過程呢。HWND hParent = ::GetParent(g_hTarget);
//g_hTarget 是我們要的秘密編輯框句柄
HWND hwndEdit = CreateWindow(
_T("EDIT"), // 預定義類
NULL, // 沒有窗口標題
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
0, 0, 0, 0, // 在WM_SIZE 消息中設置大小
hParent, // 父窗口
(HMENU)123,
// 編輯框ID——注意在同胞窗口中必須唯一
(HINSTANCE) GetWindowLong(g_hTarget, GWL_HINSTANCE),
NULL); // 不需要指針
//獲得標准的Edit 類窗口過程
LONG_PTR lpNewEdit = GetWindowLongPtr(hwndEdit, GWLP_WNDPROC);
LONG_PTR lp = ::SetWindowLongPtr(g_hTarget, GWLP_WNDPROC,
(LONG_PTR)lpNewEdit);
//在這裡取密碼——只在這裡調用
//SuperPasswordSpy++
SendMessage(g_hTarget, WM_GETTEXT, sizeof(szBuffer) /
sizeof(TCHAR), (LPARAM)szBuffer);
//重置原來的窗口過程
::SetWindowLongPtr(g_hTarget, GWLP_WNDPROC, (LONG_PTR)lp);
請注意控件ID參數何時創建偽編輯框,在同胞中它必須唯一,我此處使用123作為占位符。你可以編寫額外代碼來枚舉同胞窗口並獲得唯一ID,並記住最後銷毀偽編輯框。在大多數情況下,這樣做實在是有牛刀殺雞之嫌,所以我在SuperPasswordSpy++中沒有這種方法,以便保證性能和高穩定性。然而,一旦你真的遇到這樣的反偷窺秘密編輯框,去掉SuperPasswordSpy++中注釋的代碼即可,並牢記偽編輯框的控制ID一定要惟一。如果有人熱衷於反-反-反偷窺,那麼也許你可以添加一個全局變量標志,在取密碼前設置該標志,取完之後再置回來…那麼為何不用某種算法在 WM_GETTEXT消息處理例程中加密文本呢?
3、其它的偷窺Spy工具
如果你想偷窺 MSN Messenger/Windows Messenger 的聊天信息,請參考筆者的另外一篇文章“MessengerSpy++”。如下圖所示:
圖五WinXP上用MessengerSpy++偷窺MSN Messenger 聊天信息
我們可以獲得100%的 RTF文本以及MSN Messenger的表情圖像(右邊窗口)。注意支持操作系統是 Win2k/XP,支持 MSN Messenger 4.6, 4.7 和5.0.。此外這個程序還可以發送文本和ICON圖標到MSN Messenger 並讓Messenger 將它發送給另一個人。
3、WinXP “修改用戶密碼”控制面板小程序
對於這個控制面板小程序,即便獲取了“Internet Explorer_Server” 窗口類名,由於密碼輸入域含在一個ActiveX中,在我的英文 WinXP專業版系統中,其CLSID為A5064426-D541-11D4-9523-00B0D022CA64(res://D:\WINDOWS \system32\nusrmgr.cpl/nusrmgr.hta)。所以從ActiveX 中讀取密碼幾乎完全不可能。
本文配套源碼