偷窥桌面程序和IE浏览器的密码编辑框

环境

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 页面上的密码输入框:

程序架构

在你围绕屏幕拖动放大镜,程序便捕获鼠标位置并跟踪鼠标的移动,只要鼠标移到某个新窗口,程序便检查该窗口的类名以及窗口式样,确定窗口是否为密码编 辑框或IE(IE实际上是一个浏览器控件,为方便起见叫IE)。如果是IE,那么钩子DLL必须被立即注入到IE中,以确定IE是否包含密码输入域。

我们有两种可选的方法来实现钩子:

  • 第一种方法是设置 WH_GETMESSAGE 钩子,可以参考 Brian Friesen 的文章和代码:PasswordSpy(http://www.codeguru.com/samples/pwdspy.html)。该钩子DLL对消息进行截获并应用同步对象(互斥,事件等)。如下图:

    上图涉及到五个步骤:
    1. Spy程序将钩子DLL注入到目标程序;
    2. Spy程序发送(post)一个用户消息到目标程序,该消息将被注入的DLL截获;
    3. DLL截获用户消息并读取密码编辑框内容;
    4. DLL将数据发回到Spy程序;
    5. 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只运作一次。而第二种方法, 钩子要运作两次,这造成一定的开销(本文本程序可以忽略这种开销)。

实现细节描述

如何从浏览器控件窗口句柄获取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++的一张截图,该对话框(00060294)有两个浏览器,只有浏览器000202D6导航到某些URL。你可以做个实验,在对话框中放一个WebBrowser ActiveX,用 Spy++看看结果。

当前页面是否包含密码输入域:

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;

}

从当前页面密码输入域吸取密码:

  _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++ 实现中的其它一些细节:

判断密码编辑框的条件

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++的相关代码。

当HTML页面文件中存在多个 Frame 标签时,SuperPasswordSpy++目前的版本假设在HTML页面文件如果包含密码输入域,它只有一个 Frame。对于大多数有密码输入域的页面来说(如MSN Hotmail),SuperPasswordSpy++都能正常运行。但为了做得更完美些,我会在下个版本的SuperPasswordSpy++中添加对多个 Frame 的支持。请随时到我的站点检查更新。

其它说明

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服务)。

编辑框的偷窥与反偷窥

有人如何反偷窥。其实不需要花费太大的功夫就可以实现反偷窥。下面的代码例子使用MFC,首先派生一个 CEdit,我最初想改写PreTranslate函数,就像下面这样:

BOOL CAntiPeekEdit::PreTranslateMessage(MSG* pMsg)

//这个方法不灵!

{

   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);

}

可惜 if 语句中的代码不会被调用,为什么呢?只有MFC 开发团队知道。没办法我把注意力转到函数 WindowsProc,这样就可以行得通了。

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消息处理例程中加密文本呢?

其它的偷窥Spy工具

如果你想偷窥 MSN Messenger/Windows Messenger 的聊天信息,请参考笔者的另外一篇文章“MessengerSpy++”。如下图所示:

我们可以获得100%的 RTF文本以及MSN Messenger的表情图像(右边窗口)。注意支持操作系统是 Win2k/XP,支持 MSN Messenger 4.6, 4.7 和5.0.。此外这个程序还可以发送文本和ICON图标到MSN Messenger 并让Messenger 将它发送给另一个人。

WinXP “修改用户密码”控制面板小程序

对于这个控制面板小程序,即便获取了“Internet Explorer_Server” 窗口类名,由于密码输入域含在一个ActiveX中,在我的英文 WinXP专业版系统中,其CLSID为A5064426-D541-11D4-9523-00B0D022CA64(res://D:\WINDOWS \system32\nusrmgr.cpl/nusrmgr.hta)。所以从ActiveX 中读取密码几乎完全不可能。

你可能感兴趣的:(浏览器)