65.9K
CodeProject 正在变化。 阅读更多。
Home

COM 接口挂钩及其应用 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (46投票s)

2003 年 10 月 20 日

18分钟阅读

viewsIcon

692089

downloadIcon

8732

与 MSN Messenger 6.0 的交互

引言

自从 2002 年秋季在 www.CodeGuru.com 上发布了《MessengerSpy++ for MSN Messenger / Window Messenger》(用于与 MSN Messenger 4.6、4.7 和 5.0 交互)以来,将近一年过去了。随着微软于 2003 年 7 月发布了 MSN Messenger 6.0,它采用了与之前版本不同的架构,导致我的 MessengerSpy++ 无法与此 6.0 版本协同工作,因此我决定写这篇文章来演示如何制作一个能与 MSN Messenger 6.0 交互的程序。这次,我想向您介绍两个新内容——COM 接口挂钩和 COM 接口方法挂钩。是的,就是 COM 接口挂钩和方法挂钩,这意味着您的接口方法可以在路由到被挂钩的接口方法之前接管函数调用,就像您可能已经知道的 API 挂钩和 Windows 消息挂钩一样。

在继续阅读之前,我强烈建议您阅读我之前的文章《MessengerSpy++ for MSN Messenger / Window Messenger》以及《键盘记录器及更多》系列,第 2 篇,以熟悉 MSN Messenger 的相关话题,这将极大地帮助理解下面的讨论,因为我将故意省略这些文章中已有的内容以节省篇幅。

注意:本文也可以在此 找到,可能更具信息量且更精简。

开始前的准备

Depends, MS Platform Core SDK, MSN Messenger 6.0, Resource Hacker, Process ExplorerProcess Spy。仅限 Win 2K/XP/2003!(需要本地管理员身份);

您需要两个有效的 MSN(Passport)登录名来模拟聊天或与朋友一起试验(因为您必须开始一个聊天)。供参考:您可以使用 WinXP/2003 的快速用户切换(FUS)功能在同一台物理机上模拟多个 MSN 用户登录。

MSN Messenger 6.0 的背景和新特性

正如您可能知道的,MSN Messenger 5.0 及之前的版本使用“RichEdit”公共控件作为聊天输入区和聊天内容区,“发送”按钮是一个真正的“BUTTON”窗口控件。要与它交互,您的程序会使用挂钩或其他远程注入方式渗透到 MSN Messenger 的进程空间,并像我们在编写对话框式 GUI 程序时那样进行按钮点击和文本读取。

但是,在 MSN Messenger 6.0 中,当您使用 SPY++ 检查其窗口布局时,只有一个“DirectUIHWND”窗口。“DirectUIHWND”自 Windows XP 出现以来,根据我的观察,是一个广泛使用的包装窗口类。如果您正在使用 WinXP/2003,您可以修改 Keith Brown 先生 于 2000 年 2 月在 MSJ 上发布的工具 CmdRunAs,或者 Martyn 'Ginner' Brown 先生 在 www.codeguru.com 2001 年发布的工具“Start a Command As Any User”,或者如果您是懒惰的打字员,可以直接使用我的 基于 GUI 的“RunAs”,以“LocalSystem”账户启动 SPY++ 到您的登录桌面(WinSta0\Winlogon)。

注意*

  1. 您必须是管理员组的成员才能这样做。
  2. 无需尝试 WinXP/2003 的“Runas...”Shell 命令,因为它始终在当前桌面(WinSta0\Default)中启动。切换到登录桌面时,您可能需要按“Alt+Esc”才能使 SPY++ 可见。

现在您会找到“DirectUIHWND”窗口。为了方便非 WinXP 用户,这里是此场景的屏幕截图。

因此,与 MSN Messenger 6.0 交互的关键在于是否能够成功挂钩到“DirectUIHWND”并从中获取数据。一般而言,如果您没有其内部 Windows 消息处理程序、COM 接口描述和某些全局数据结构的文档,那么挂钩和与“DirectUIHWND”这样的包装窗口交互几乎是不可能的。但幸运的是,在使用 Process Spy 和 Depends 之后,很清楚 MSN Messenger 6.0 会动态加载和卸载“RichEd20.DLL”,这给了我们一个信号,表明它内部使用了无窗口 Rich Edit 控件。

(供参考:您可能知道,richedit20.dll 是 Worm.Nimda 的目标,当用户启动 MS Office 系列应用程序时,它会自我启动。此外,在发布伴随 OfficeXP 的新版本之前,它存在一个严重的缓冲区溢出问题,导致远程用户通过 IM 聊天接管系统。现在,它默认受到“Protected Storage”NT 服务的保护。尝试覆盖此文件将失败,除非您首先停止此 NT 服务。)

现在,使用 Depends 打开“richedit20.dll”。默认情况下,此文件位于“%SystemRoot%\system32”目录。(如果您使用的是 Win2k,它可能在 C:/WinNT/System32;或者,如果您使用的是 WinXP,则在 C:/Windows/System32。)导出的表如下所示:

序号 ^ 提示 函数 入口点
2 (0x0002) 1 (0x0001) IID_IRichEditOle 0x00014C60
3 (0x0003) 2 (0x0002) IID_IRichEditOleCallback 0x00014C50
4 (0x0004) 0 (0x0000) CreateTextServices 0x0000D882
5 (0x0005) 5 (0x0005) IID_ITextServices 0x00014C20
6 (0x0006) 3 (0x0003) IID_ITextHost 0x00014C30
7 (0x0007) 4 (0x0004) IID_ITextHost2 0x00014C40
8 (0x0008) 6 (0x0006) REExtendedRegisterClass 0x0004BA5C
9 (0x0009) 7 (0x0007) RichEdit10ANSIWndProc 0x0003DB01
10 (0x000A) 8 (0x0008) RichEditANSIWndProc 0x00015681

对我们来说,riched20.dll 只导出了不到十二个变量和函数,这真是太幸运了。此外,在我用“>Dumpbin -Exports Riched20.DLL”检查它之后,它没有前向函数,这对我们挂钩者来说是个好消息;而且默认情况下,riched20.dll 不在注册表中的“已知 DLL”部分。所有这些事实决定了我们只需要挂钩这个 riched20.dll

注意:我并不是说“前向函数”和“已知 DLL”会阻止我们挂钩;您始终可以在最终的 DLL 上挂钩 Windows API。现在,我们没有这两个问题,这意味着我们只需要处理一个 riched20.dll,工作量会减少。

此外,当我说 MSN Messenger 6.0 中的加载代码如下(伪代码)时,您可能会感到惊讶:

HMODULE hRichEditLib = ::LoadLibrary(_T("RICHED20.DLL"));
而不是更健壮的代码
TCHAR szLibPath[MAX_PATH];
UINT uRet = ::GetSystemDirectory(szLibPath, MAX_PATH]);
if(uRet == 0) err;
szLibPath[uRet] = TCHAR('\0');
::lstrcat(szLibPath, _T("\RichEd20.DLL"));
HMODULE hRichEditLib = ::LoadLibrary(szLibPath);

好的,现在启动您的 VC++ 6.0 或 VS.NET,创建一个名为“riched20.dll”(不区分大小写)的新 Win32 DLL 项目,将其设置更改为 Unicode,并在 riched20.def 文件中导出与上面表格中显示的完全相同的项,如下所示:

LIBRARY "Riched20"

DESCRIPTION 'RichEdit Ver 2 & 3 DLL'
EXPORTS
IID_IRichEditOle          @2 PRIVATE
IID_IRichEditOleCallback  @3 PRIVATE
CreateTextServices        @4 PRIVATE
...

您需要转到 Platform SDK(现在称为 MS SDK)目录,找到“TextServ.h”文件,并从中复制以下内容,以创建一个名为“MyTextServ.h”的新文件到您的项目中,如下所示:

#ifndef _TEXTSERV_H
#define _TEXTSERV_H
#ifdef __cplusplus
struct PARAFORMAT2 : _paraformat    //Copied from RichOle.h,
                                    //RichEdit.h...
{
   LONG dySpaceBefore;
   ...
};
#else // Regular C-style
typedef struct _paraformat2
{
  UINT cbSize;
  ...
} PARAFORMAT2;
#endif // C++

//... more enum, struct and constant definition copied
// from RichOle.h
struct CHANGENOTIFY {
    DWORD dwChangeType;
    void * pvCookieData;

};

#define TXTBIT_RICHTEXT 1
#define TXTBIT_MULTILINE 2
...

class ITextServices : public IUnknown
{
  public:
     //@cmember Generic Send Message interface
    virtual HRESULT TxSendMessage(
      UINT msg,
      WPARAM wparam,
      LPARAM lparam,
      LRESULT *plresult) = 0;
    //more virtual functions ....
};

class ITextHost : public IUnknown
{
  public:
    //@cmember Get the DC for the host
    virtual HDC TxGetDC() = 0;
    //@cmember Release the DC gotten from the host
    virtual INT TxReleaseDC(HDC hdc) = 0;
    ...
    //more functions ...
};
//+---------------------------------------------------------------
// Factories
//----------------------------------------------------------------
// Text Services factory

STDAPI CreateTextServices(
  IUnknown *punkOuter,
  ITextHost *pITextHost,
  IUnknown **ppUnk);

typedef HRESULT (STDAPICALLTYPE * PCreateTextServices)(
  IUnknown *punkOuter,
  ITextHost *pITextHost,
  IUnknown **ppUnk);

#endif    // _TEXTSERV_H

您可能会发现将这个伪头文件编译到我们的模块中很麻烦。您需要深入研究几个头文件并复制粘贴您需要的常量、枚举或结构。

[不要包含“RichOle.h”或“RichEdit.h”!!!我们在创建一个伪 RichEd20.DLL;这就是为什么您必须确保将这些头文件中定义的项添加到我们的“MyTextServ.h”中。此外,永远不要更改类中的成员函数顺序!!!这会破坏我们依赖的 Vtbl 以进行 COM 接口挂钩。]

相应地修改您的 riched20.h 文件,使其导出这些:

#define RICHED20_API __declspec(dllexport)
extern "C" RICHED20_API GUID IID_IRichEditOle;
extern "C" RICHED20_API GUID IID_IRichEditOleCallback;
...

#include <unknwn.h>
#include "mytextserv.h"

extern "C" HRESULT WINAPI CreateTextServices(IUnknown *punkOuter, 
     ITextHost *pITextHost, IUnknown **ppUnk);
...

相应地修改您的 riched20.cpp 文件:

RICHED20_API GUID IID_IRichEditOle = { 0x00020D00, 0x0, 0x0, 
  { 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46 } };
...
typedef HRESULT (__stdcall *lpCreateTextServices)(IUnknown *punkOuter, 
   ITextHost *pITextHost, IUnknown **ppUnk);
typedef LRESULT (__stdcall *lpREExtendedRegisterClass)(HWND hWnd, 
   UINT Msg, WPARAM wParam, LPARAM lParam);
typedef LRESULT (__stdcall *lpRichEdit10ANSIWndProc)(HWND hWnd, UINT Msg, 
   WPARAM wParam, LPARAM lParam);
typedef LRESULT (__stdcall *lpRichEditANSIWndProc)(HWND hWnd,UINT Msg, 
   WPARAM wParam, LPARAM lParam);

#define NEW_DLL_NAME _T("\\RichEd20.Dll")

//You MUST dynanically load the DLL
HRESULT WINAPI CreateTextServices(IUnknown *punkOuter, 
   ITextHost *pITextHost, IUnknown **ppUnk)
{
   TCHAR szLib[MAX_PATH]; //255 is enough
   DWORD dw = GetSystemDirectory(szLib, MAX_PATH);
   if(dw == 0) return 0;
   szLib[dw] = TCHAR('\0');
   ::lstrcat(szLib, NEW_DLL_NAME);
   HMODULE hLib = LoadLibrary(szLib);
   if(!hLib) return 0;
   lpCreateTextServices _CreateTextServices = 
          (HRESULT (__stdcall *)(IUnknown*, 
          ITextHost*, IUnknown**)) 
      ::GetProcAddress(hLib, "CreateTextServices");
   if(!_CreateTextServices) return 0;
   HRESULT hr = (_CreateTextServices)(punkOuter, pITextHost, ppUnk);
   //We cache this COM interface
   ITextServices* lpTx;
   ((IUnknown*)(*ppUnk))->QueryInterface(IID_ITextServices, (void**)(&lpTx));
   if(lpTx)
     MessageBox(NULL, _T("Interface Hooked"), _T("Indeed"), MB_OK);
   //::FreeLibrary(hLib); //NOT FREE IT!!!
   return hr;
}
...

在正确放置所有内容之前,使此 DLL 可编译可能需要您花费一些时间。在生成此伪 riched20.dll 后,将其复制到 MSN Messenger 6.0 目录。启动 MSN Messenger 6.0,开始一个聊天,您会发现每个聊天窗口都会弹出六次消息框。这意味着每个聊天窗口中有六个无窗口的 rich edit 控件在为您服务。经过几次实验,我知道第一个是显示聊天者电子邮件地址和昵称的地址区域。第二个是聊天内容区,第四个是您输入文字的地方。其他区域没有直接的用户交互功能,因此我们在后续讨论中省略它们。

COM 接口挂钩?

到目前为止,代码大师们应该已经理解了我们所有工作的核心,就像我在我的 MessengerSpy++ 中所做的那样,我获取了聊天区域的窗口句柄并与之交互。这次,我将我的类挂钩到无窗口的 rich edit 中,并“实体化”它;也就是说,您获取了实际存在的接口指针。这种“实体化”必须在 COM 接口指针返回给应用程序(MSN Messenger)之前完成,并且我的模块必须是一个注入到该应用程序中的 DLL 模块。

这样,我的代码就可以(希望不会有竞态条件、同步冲突等,经过仔细设计代码后)安全地利用 COM 接口与应用程序交互。酷的是,您可以更进一步,修改接口的 Vtbl——让应用程序的调用先进入我们的挂钩函数,而不是调用被窃取的 COM 接口方法。以下图示将解释普通 COM 接口方法调用和挂钩接口方法调用之间的区别。

如果您对此仍然不清楚,请参考上面的图示。看,MSN Messenger 6.0 实现的 ITextHost 接口被传递给 riched20.dll,并返回了一个 ITextService 接口指针。因为我们的伪 Riched20.dll 处于中间位置,我们现在拥有了接口的指针——一方面,您可以查询 ITextService 以获取 RTF 数据,甚至设置 RTF 数据(这是动态与 MSN Messenger 6.0 聊天交互的前提条件)。另一方面,您可以“WriteProcessMemory”并修改接口的虚拟表(如果将其应用于 TxSendMessage,这相当于 Windows 消息挂钩)。

与 API 挂钩和消息挂钩相比,COM 接口挂钩有一个致命的缺点:您可以通过修改内存中的进程映像来自由地进行 API 挂钩和取消挂钩。您也可以通过简单地调用“SetWindowsHookEx”和“UnhookWindowsHookEx”来自由地挂钩和取消挂钩 Windows 消息。注意,自由意味着您可以在任何时候进行挂钩/取消挂钩,包括目标进程已经运行了一段时间。COM 接口挂钩 just cannot accept this。您必须在 COM 接口指针诞生时捕获它,否则您将永远失去它。

这种特性意味着 COM 接口挂钩程序必须在目标程序创建接口指针之前运行,有时如果目标程序在最开始就进行所有接口创建工作,它必须密切关注目标进程的创建。这意味着您可能需要编写 DDK 驱动程序来安装“进程监视器”,或者使用其他可能的方式,比如我们这次放置一个伪 DLL。至于 COM 接口方法挂钩,在某些情况下,在不触发死锁或竞态条件的情况下保持程序稳定性可能会极其困难。所以要小心。

此外,大多数时候,您可能需要直接挂钩 CoCreateIntance(Ex) 来获取接口指针,这需要您首先掌握 API 挂钩技术。作为一个快速链接集合点,您可能会发现我的《键盘记录器及更多,第 3 部分》提供了大量关于此主题的资源。此主题将在本系列的后续文章中再次讨论。

通信解决方案——MSN Messenger 6.0 与您的程序之间

现在,让我们来谈谈您的程序与伪 riched20.dll 之间的通信。您可以参考我之前的文章,《MessengerSpy++》和《键盘记录器及更多系列,第 1 部分》关于在使用 MMF 处理可变长度单向数据传输的用法。虽然您可以在这个伪 DLL 中启动一个“监听”线程并从您的程序接收命令,但这可能会很复杂且容易出错。就像我在 MessengerSpy++ 中使用 Windows 用户消息一样,这次我采用了相同的方法,并进行了一些修改。

因为我没有对 MSN Messenger 6.0 的聊天窗口使用“SetWindowsHookEx”(嗯,您可以使用它,但意义不大,就像我在 MSN Messenger 5.0 中使用它来处理窗口句柄一样),我创建了一个隐藏窗口,这个隐藏窗口就像 COM STA 一样,负责您的查询同步、命令处理和重定向。代码会非常紧凑;30 行就足够了。“简单就是美。”下面的代码片段

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call,
    LPVOID lpReserved)
{
   switch (ul_reason_for_call)
   {
      case DLL_PROCESS_ATTACH:
        InitializeRecv(TRUE); //Init the hidden window
        break;
      case DLL_THREAD_ATTACH:
        break;
      case DLL_THREAD_DETACH:
        break;
      case DLL_PROCESS_DETACH:
        InitializeRecv(FALSE);
        break;
   }
   return TRUE;
}

BOOL InitializeRecv(BOOL bInitialize)
{
   if(bInitialize)
   {
      //Create Window....
      RegisterClassEx(&wcex)
      g_hRecvWnd = CreateWindow(...);
   }
   else
   {
      if(!::IsWindow(g_hRecvWnd))
        return FALSE;
      ::PostMessage(::g_hRecvWnd, WM_CLOSE, 0, 0);
      ::g_hRecvWnd = NULL;
   }
   return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
     LPARAM lParam)
{
   switch (message)
   {
      case WM_DESTROY:
        PostQuitMessage(0);
        break;
      case WM_YOUR_COMMAND_QUERY_CHAT_AS_TEXT:
        BSTR bstrChat;
        g_lpTextService[wParam]->TxGetText(&bstrChat);
        WriteChatTextToMMF(bstrChat);
        SysFreeString(&bstrChat);
        break;
      default:
      return DefWindowProc(hWnd, message, wParam, lParam);
   }
   return 0;
}

您可能因为我讲了这么久的理论内容而感到厌烦。我也是。所以,我将在此停止,让您尝试附带的演示程序。使用“Riched20 Ver1”文件夹,并将编译好的 DLL 复制到您的 MSN Messenger 6.0 文件夹中。确保在开始聊天之前完成此操作。现在,与朋友(或使用 WinXP/2003 的您自己)开始聊天。我在隐藏窗口中添加了一个计时器,所以每 10 秒它会弹出一个消息框,显示“To-Send”编辑框中的内容。

MSN Messenger 6.0 表情图标的处理程序

另外,尝试将一些表情图标与文本放在一起。为了保持简单,我只将第一个表情图标保存到您的 C 盘根目录下作为一个位图文件。根据表情图标的本质,它是一个 WMF 文件,但是,如果我在这里向您展示其原始尺寸的图像,我猜您会更喜欢使用位图。

图示。从 MSN Messenger 6.0 抓取的表情图标位图(尺寸:始终为 19 X 19 像素)

图示。从 MSN Messenger 6.0 抓取的表情图标 WMF 文件。注意其尺寸远大于其位图对应文件(尺寸:始终为 200 X 200 像素)。

以下是提取 MSN Messenger 6.0 中表情图标的代码。它与 MessengerSpy++ 有些类似,但这次嵌入的对象是 WMF 格式而不是 BMP 格式。

//Say, now, you have ITextServices* pointer g_lpIText already
BSTR bstr;
HRESULT hr = ((ITextServices*)::g_lpIText->TxGetText(&bstr);
if(FAILED(hr)) err;
//Process the text you got
::SysFreeString(bstr);
//I only deal with the first embedded emotional icon
IRichEditOle* pReo = NULL;
g_lpIText->TxSendMessage(EM_GETOLEINTERFACE, 0, 
    (LPARAM)(LPVOID*)&pReo, &lr);
if(lr == 0) return;
//how many images do we have?
LONG nNumber = pReo->GetObjectCount(); //Your Image's Number
//remember to pReo->Release(); when everything is settled
if(nNumber == 0) return;
REOBJECT* ro = new REOBJECT;
ro->cbStruct = sizeof(REOBJECT);
//deal with first image
hr = pReo->GetObject(0, ro, REO_GETOBJ_ALL_INTERFACES);
if(FAILED(hr)) err;
IDataObject* lpDataObject;
hr = (ro->poleobj)->QueryInterface(IID_IDataObject, (void **)&lpDataObject);
if(FAILED(hr)) err;

//I was stuck here for a while
//ParseDataObject(lpDataObject);

STGMEDIUM stgm; // out
FORMATETC fm; // in
fm.cfFormat = CF_METAFILEPICT; // Clipboard format
fm.ptd = NULL; // Target Device = Screen
fm.dwAspect = DVASPECT_CONTENT; // Level of detail = Full content
fm.lindex = -1; // Index = Not appliciple
fm.tymed = TYMED_MFPICT;
hr = lpDataObject->GetData(&fm, &stgm);
if(FAILED(hr)) err;
//Metafile handle. The tymed member is TYMED_MFPICT.
HMETAFILEPICT hMetaFilePict = stgm.hMetaFilePict;
LPMETAFILEPICT pMFP = (LPMETAFILEPICT) GlobalLock (hMetaFilePict);
int cx = 19; // pMFP->xExt;
// it is always 19 X 19
int cy = 19; // pMFP->yExt;
HWND hWnd = ::GetDesktopWindow();
//You are using true color display anyway
HDC hDC = ::GetDC(hWnd);
HDC hMemDC = ::CreateCompatibleDC(hDC);
HBITMAP hMemBmp = ::CreateCompatibleBitmap(hDC, cx, cy);
HBITMAP hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hMemBmp);
//Draw on Mem DC
::PlayMetaFile(hMemDC, pMFP->hMF);
//If you want just save WMF anyway, just do that
CopyMetaFile(pMFP->hMF, _T("C:\\fromMSN.wmf"));
//If you want to save as BMP, go on
TCHAR szFilename[64];
wsprintf(szFilename, _T("c:\\fromMSN.bmp"));
//Hope you have C driver
HANDLE hFile = ::CreateFile(szFilename, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE) err;
DWORD dwWritten;
//need file header
BITMAPFILEHEADER bmfh;
bmfh.bfType = 0x4d42; // 'BM'
int nColorTableEntries = 0; // true color only
int nSizeHdr = sizeof(BITMAPINFOHEADER) + 
    sizeof(RGBQUAD) * nColorTableEntries;
bmfh.bfSize = 0;
bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) +
  sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries;
::WriteFile(hFile, (LPVOID)&bmfh, sizeof(BITMAPFILEHEADER), 
    &dwWritten, NULL);
BITMAP bm;
//get bitmap information
::GetObject(hMemBmp, sizeof(bm), &bm);
int nBitCount = bm.bmBitsPixel; //Warning! True Color!
BITMAPINFOHEADER* lpBMIH = (LPBITMAPINFOHEADER) new
  char[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries];
lpBMIH->biSize = sizeof(BITMAPINFOHEADER);
lpBMIH->biWidth = bm.bmWidth;
lpBMIH->biHeight = bm.bmHeight;
lpBMIH->biPlanes = 1;
lpBMIH->biBitCount = nBitCount;
lpBMIH->biCompression = BI_RGB;
lpBMIH->biSizeImage = 0;
lpBMIH->biXPelsPerMeter = 0;
lpBMIH->biYPelsPerMeter = 0;
lpBMIH->biClrUsed = nColorTableEntries;
lpBMIH->biClrImportant = nColorTableEntries;

//Compute Image Size
DWORD dwCount =((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount)/ 32;
if(((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount) % 32)
dwCount++;
dwCount *= 4;
dwCount = dwCount * lpBMIH->biHeight;
//Use Virtual Memory API instead of new-delete
LPVOID lpImage = ::VirtualAlloc(NULL, dwCount, MEM_COMMIT, PAGE_READWRITE);
BOOL result = GetDIBits(hMemDC, (HBITMAP)hMemBmp, 0L, // start scan line
  (DWORD)bm.bmHeight, // # of scan lines
  (LPBYTE)lpImage, // address for bitmap bits
  (LPBITMAPINFO)lpBMIH, // address of bitmapinfo
  (DWORD)DIB_RGB_COLORS // use rgb for color table
);

::WriteFile(hFile, lpBMIH, sizeof(BITMAPINFOHEADER), &dwWritten, NULL);
::WriteFile(hFile, lpImage, dwCount, &dwWritten, NULL);
::VirtualFree(lpImage, 0, MEM_RELEASE);
::CloseHandle(hFile);
//Restore DC
::SelectObject(hMemDC, hPrevBmp);
::DeleteObject(hMemBmp);
::DeleteDC(hMemDC);
::ReleaseDC(hWnd, hDC);
::GlobalUnlock(hMetaFilePict);
//do not forget COM household work today
ro->poleobj->Release(); //GetObject Called AddRef so
//Release here
delete ro;
您可能会想:嗨,您怎么知道它是 WMF 格式?不幸的是,我不是先知,我尝试了多种方法来揭示 MSN Messenger 团队隐藏在幕后的内容。幸运的是,我做到了。下面是我的代码:

void ParseDataObject(IDataObject* lpDataObject)
{
    DWORD dwCF[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
       15, 16, 17, 0x0080, 0x0081, 0x0082, 0x0083, 0x008E};
    DWORD dwTM[] = {1, 2, 4, 8, 16, 32, 64, 0};
    int dimCF = sizeof(dwCF)/sizeof(dwCF[0]);
    int dimTM = sizeof(dwTM)/sizeof(dwTM[0]);
    for(int i = 0; i < dimCF; i++)
    {
       for(int j = 0; j < dimTM; j++)
       {
          FORMATETC fm; // in
          fm.cfFormat = dwCF[i]; // Clipboard format 
          fm.ptd = NULL; // Target Device = Screen
          fm.dwAspect = DVASPECT_CONTENT;
          fm.lindex = -1; // Index = Not appliciple
          fm.tymed = dwTM[j];
          STGMEDIUM stgm; // out
          HRESULT hr = lpDataObject->GetData(&fm, &stgm);
          if(FAILED(hr)) continue;
          PopMsg(_T("I caught it %d, %d"), i, j);
       }
    }
}

关注点——挂钩 COM 接口方法

对于我们 C++ 人来说。COM 接口就是一个 C++ 类(是的,我知道 COM 是语言中立的,但在这里把它当作普通的 C++ 类更有意义)。它派生自 IUnknown,肯定有一个虚函数表(简称 VTBL),因为基类已经有一个虚函数了。我知道几乎所有人都已经有这方面的经验了,但我认为很少有人清楚 VTBL 在内存中是如何存在的。

实际上,不同的 C++ 编译器有不同的实现方式(我不知道像 Delphi 和 C++ Builder 这样一些流行的编译器是如何做的,但我猜它们与 Visual C++ 采取了类似的方法。但是,有一点是它们都在 MS Windows 上运行。据我所知,至少有一种用于 DSP 芯片编程的 C++ 编译器在类成员之后放置一个 vtbl 指针,而 MSVC 将 vtbl 指针放在第一个位置)。我们在这里讨论的是特定的 MSVC 在 Win32 平台上的情况,所以这里的所有指针都是 4 字节长。使用以下代码(您将在附带演示的 VtblStory1 文件夹中找到它):

class classA
{
public:
   virtual int method1() { return 11; }
   virtual int method2() { return 12; }
   virtual int method3() { return 13; }
};

class classB: public classA
{
public:
   virtual int method1() { return 21; }
   int m;
   int n;
};

class classC : public classB
{
public:
   int method1(int a, short b) { return 31; }
};

void testVtabl()
{
   classC* pC = new classC;
   classB* pB = pC;
   int y = pB->method1(); //31

   classB bb;
   bb.m = 31;
   bb.n = 32;
   LPVOID pBB = &bb;
   LPVOID pBB2 = &(bb.m);
   LPDWORD* lpVtabl = (LPDWORD*)&bb;
   HANDLE hSelf = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 
       ::GetCurrentProcessId());

   MEMORY_BASIC_INFORMATION mbi;
   if(VirtualQueryEx(hSelf, (LPVOID)(*lpVtabl), 
          &mbi, sizeof(mbi)) != sizeof(mbi)) err;

   PVOID pvRgnBaseAddress = mbi.BaseAddress;
   DWORD dwOldProtect1, dwOldProtect2;
   if(!::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, PAGE_EXECUTE_READWRITE, 
            &dwOldProtect1)) err;
   BOOL bStridePage = FALSE; //Check if Vtbl Strike 2 Pages
   LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress;
   lpByte += 4096; //in Win32, 4k/page
   if((DWORD)lpByte < (DWORD)lpVtabl + 4) //We explain later
   bStridePage = TRUE;

   PVOID pvRgnBaseAddress2 = (LPVOID)lpByte;
   if(bStridePage)
      if(!::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, 
             PAGE_EXECUTE_READWRITE, &dwOldProtect2)) err;
   //Swap classB's method1 & method2 pointer
   DWORD dw;
   memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl), 4);
   memcpy((LPVOID)(*lpVtabl), (LPVOID)(*lpVtabl + 1), 4);
   memcpy((LPVOID)(*lpVtabl + 1), (LPVOID)&dw, 4);
   //recover page property
   DWORD dwFake;
   ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, dwOldProtect1, &dwFake);
   if(bStridePage)
      ::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, dwOldProtect2, 
               &dwFake);
   //Compiler sometimes addicts to optimization
   y = bb.method1(); //still 21
   y = bb.method2(); //22
   //Unfortunatly Compile takes place one step earlier; you will
   //not see effect
   return;
}

在 MSVC++ 中,如果一个类本身或其基类有一个虚函数,它在内存中的类布局的前 4 个字节是 Vtbl 的指针,然后是成员变量,然后是成员函数。以图为例:pBB 指向 classB 的一个实例,转到内存窗口。前 4 个字节是“2C 50 42 00”(记住 Intel 芯片是小端序),后面是“1F 00 00 00”。这就是我们刚分配给 m 的值 31 (0x1F),然后是“20 00 00 00”,这就是我们分配给 n 的值 32 (0x20)。

现在,转到虚拟内存 0x 0042 502C,并查看最右边的内存窗口。嗯,怎么说呢,从 0042502C 到 00425038 是 classB 的虚拟表区域,从第一个虚拟方法——method1 开始,它是“28 10 40 00”,这与基类——classA 的 method1 不同,classA 的 method1 位于 0x0042503C。这是有道理的;当您调用 classB 的 method1 时,您进入了它的 method1。另一方面,因为 classB 没有实现 method2,当您在 classB 实例中调用 method2 时,您会进入 classA 的 method2。

当您滚动垂直滚动条时,您会看到 classC 的 vtbl 位于更高的连续内存区域(左下角的内存窗口)。通过仔细比较此区域中的数据,我敢打赌您现在对 vtbl 的布局很清楚了。

请注意:Vtbl 总是被放在只读页面中,与您在 C++ 中声明的任何 const 一起,如果您尝试写入它,您的程序将被系统终止,并弹出一个 GP 错误框,这意味着您必须在交换 classB 的 Vtbl 中 method1 和 method2 的指针之前调用 VirtualProtectEx 来将页面属性修改为 PAGE_EXECUTE_READWRITE

另请注意:没有证据表明一个类的 Vtbl 位于单个页面中。您必须确保您的所有写入操作都在您已修改的区域中进行。在上面的代码中,总共有三个虚拟方法,所以 classA、classB、classC(它们是连续的)的 vtbl 各占 3 * 4 字节,这就解释了为什么“if((DWORD)lpByte < (DWORD)lpVtabl + 4)”,看,lpByte 指向 classB 的 Vtbl,我想确保在继续之前修改 classB 的 Vtbl 区域。

现在您可能会问:“嗨,我改变了 classB 的 Vtbl,好的,然后我期望‘(y = bb.method1()) == 22’和‘(y = bb.method2()) == 21’。为什么我仍然得到 21 和 22?答案是:编译器已经计算了函数入口并将其硬编码在二进制文件中。换句话说,当程序启动时,它根本没有使用 Vtbl,因为编译器在编译时就已经决定了调用哪个函数。唉,太聪明的编译器...

那么,我们到底能做什么才能让程序在调用成员函数之前查看 Vtbl 呢?面向组件的程序。因为对于组件来说,它不知道在运行时会调用哪个成员函数,它必须使用 Vtbl 来决定调用哪个成员函数。好的,让我们创建一个这样的场景:(我不教 COM/ATL;您必须有这方面的经验才能继续。)

创建一个 COM ATL DLL 项目(您将在附带演示的 Plus 文件夹中找到其源代码),使用所有默认设置,需要代理/存根绑定,添加一个 Simple Object,称之为 Sum,将其设为 Custom Interface(您可以选择 Dual,但之后您需要修改偏移量),并添加两个方法,直到您在 Sum.cpp 中得到以下内容:

STDMETHODIMP CSum::method1()
{
   PopMsg(_T("method1"));
   return S_OK;
}

STDMETHODIMP CSum::method2()
{
   PopMsg(_T("method2"));
   return S_OK;
}

不用担心 PopMsg;它只是调用 WinAPI MessageBox。然后,添加第三个方法:

STDMETHODIMP CSum::RadarIt()
{
   LPDWORD* lpVtabl = (LPDWORD*)this;
   HANDLE hSelf = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
          ::GetCurrentProcessId());
   MEMORY_BASIC_INFORMATION mbi;
   if(VirtualQueryEx(hSelf, (LPVOID)(*lpVtabl), &mbi, sizeof(mbi) 
            != sizeof(mbi)) err;
   PVOID pvRgnBaseAddress = mbi.BaseAddress;
   DWORD dwOldProtect1, dwOldProtect2;
   if(FALSE == ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, 
            PAGE_EXECUTE_READWRITE, &dwOldProtect1)) err;
   //make sure all Vtbl areas are set
   LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress;
   lpByte += 4096; //in Win32 4k/page, I am too lazy to call API
   BOOL bStridePage = FALSE;
   if((DWORD)lpVtabl + 2 * 4 > (DWORD)lpByte)
      bStridePage=TRUE;
   PVOID pvRgnBaseAddress2 = (LPVOID)lpByte;
   if(bStridePage)
      if(FALSE == VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, 
           PAGE_EXECUTE_READWRITE, &dwOldProtect2)) err;
   //Vtbl has five functions; they are
   //Add Release QueryInterface Method1 Method2
   //swap 3rd <--> 4th
   //That is swap Method1 and Method2
   DWORD dw;
   memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl + 3), 4);
   memcy((LPVOID)(*lpVtabl + 3), (LPVOID)(*lpVtabl + 4), 4);
   memcpy((LPVOID)(*lpVtabl + 4), (LPVOID)&dw, 4);
   //Recover Page Property
   DWORD dwFake;
   ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, dwOldProtect1, &dwFake);
   if(bStridePage)
      ::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, dwOldProtect2, 
              &dwFake);
   return S_OK;
}

嗨,一个小事,我太懒了,不想调用 API 来获取页面大小,尽管如果您是认真的话,您应该这样做。让我们专注于主要话题。ISum 派生自 IUnknown,它已经有 AddRefReleaseQueryInterface 的虚函数。因此,我们的 method1 和 method2 的偏移量是 3 和 4(以 DWORD 为单位,即 4 字节)。请记住,当调用 RadarIt 时,我们交换了 method1 和 method2。

现在,创建一个 MFC 对话框项目(您将在附带演示的 Pop 文件夹中找到代码),所有设置默认,导入 DLL 的类型库,在对话框上添加一个按钮,然后执行以下操作:

void CPopDlg::OnButton1()
{
   CoInitialize(NULL);
   PLUSLib::ISum* pSum;
   hr = ::CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, iid, 
          (LPVOID*)&pSum);
   pSum->method1(); //Pop up "method1"
   pSum->RadarIt();
   pSum->method1(); //Pop up "method2"
   ::CoUninitialize();
}

第一次调用 pSum->method1() 时,您将看到“method1”消息框;但调用 RadarIt 后,您调用 method1;您将收到“method2”消息框,而不是“method1”消息框。有道理吗?

好的,再进一步。现在,将 ATL DLL 的代码更改为以下内容:

STDMETHODIMP CSum::method2()
{
   PopMsg(_T("method2"));
   RadarIt();    //Recover Original Vtbl
   method1();    //to call method1()
   RadarIt();    //
   return S_OK;
}

尝试 Pop Dialog;现在,您会看到 method2 占据了 method1 的位置,实际上 method2 包装了 method1——每次调用 method1 时,您首先进入 method2,然后才是 method1。COM 接口方法已被挂钩。这样想:您的代码注入到目标程序中,获取了它使用的 COM 接口指针,挂钩了接口方法,然后……所有调用都首先进入您的代码,您可以进行修改,做任何事情,然后,您决定是否将其传递给原始接口方法。只需记住一件事:接口只能在其诞生时被挂钩。

今天的内容就到这里,我们将在下一篇文章中继续讨论 MSN Messenger 6.0 和 COM 挂钩。在玩演示程序时要小心,计时器每 10 秒会弹出一个消息框。在删除或移除我们的伪 riched20.dll 之前,您必须退出 MSN Messenger 6.0,就像在将其复制到 MSN Messenger 6.0 目录之前一样。

最后,就像本文的标题一样,这是“与 MSN Messenger 6.0 交互系列”的第一部分,我计划在接下来的文章中提供一个类似于我的“MessengerSpy++ for MSN Messenger / Window Messenger”的工具来处理 MSN Messenger 6.0。此外,我们可能会讨论一些关于 MS Office-IM 交互的内容,或者构建一个基于 MSN 网络进行 MSN Messenger 6.0 通信的 P2P 程序隧道,因为 MSN Messenger 6.0 似乎现在可以穿透防火墙了……

历史

版本历史

版本 发布日期 特点
1.0 2003 年 10 月 14 日 发布到 https://codeproject.org.cn/
0.9 2003 年 9 月 29 日 挂钩接口方法已实现
0.85 2003 年 9 月 8 日 从 MSN IM6 抓取 rtf、图标、ole 对象
0.8 2003 年 7 月 7 日 在伪 DLL 中捕获 ITextService,忙碌中……zzz…… 加上提交 CodeGuru.com 上的“Keystroke”系列
0.3 2003 年 7 月 ? 日 强制注入 API 在 MSN IM6 上失败;它故意阻止了联系人列表。但是时间系统和套接字系统被注入的代码接管了。
© . All rights reserved.