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

Windows Live Messenger 插件开发圣经

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (52投票s)

2008年11月3日

GPL3

56分钟阅读

viewsIcon

208177

downloadIcon

2684

一篇解释应用于 Live Messenger 研究和插件开发的几种 Win32 逆向工程技术的文章。

目录

简介

本文面向所有希望为 Windows Live Messenger 开发附加组件或插件的程序员。

虽然示例代码是用 C++ 编写的,但其中涉及的理论和实践概念可以在大多数编程语言中实现。另一方面,考虑到本文所揭示的大部分“技巧”适用于广泛的 Windows 应用程序,本文对于从概念上掌握在应用程序功能和互操作性研究、操作系统内部原理和/或一般逆向工程任务等领域中实用的方法和技术应该会有所帮助。

必备技能和工具

阅读本文所需的最低技能是对 Windows API、C/C++、COM 和 x86 架构有基本的了解。同时建议能够熟练使用其他开发工具,如 API 和消息监视软件、可执行文件检查工具等。强烈推荐以下工具和库,其中一些在本文中会用到:

文章布局

本文循序渐进地探讨插件开发这一主题。文章分为以下几个部分:

  1. 进入 Windows Live Messenger 地址空间
  2. 解释了如何研究和开发代理 DLL,以将代码注入目标应用程序的地址空间。其中使用了用于探索 PE 可执行文件导入和导出的工具,并提供了实现代理 DLL 调用转发存根的指南。

  3. Windows Live Messenger 破解
  4. 这是本文的主要部分,探讨了以下主题:

    • 使用 Trappola 库拦截操作系统调用。
    • 讨论 Windows Messenger 的窗口类,我们将通过向顶层菜单栏添加元素来进行首次实际的 Hook 操作。
    • 初步介绍窗口子类化的概念,稍后将通过创建与 Live Messenger 当前配色方案相匹配的自定义窗口来扩展这一概念。
    • Windows Live Messenger 如何存储和使用资源,包括描述如何使用 XML 定义 UI 元素的视觉和行为方面。分析还解释了 Windows Live Messenger 和大多数应用程序用于查找资源并通过资源地址加载它们的调用序列。
    • 解释了附加的 VC++ 项目中实现的资源管理类 (RMC) 背后的理论及其功能。通过向 Windows Live Messenger 界面添加工具栏按钮来展示该类的实际用途。
    • Microsoft Active Accessibility API、其在获取 Windows Live Messenger 控件属性方面的用途,以及可访问性事件 Hook 的理论和实践,并结合介绍了 Messenger 暴露的 COM 接口以及如何使用它们来获取联系人信息。

如何构建和使用源代码

注意此代码针对 Live Messenger 8.5。其他版本可能需要修改,尤其是在资源 Hook 方面。

源代码实现了本文中探讨的所有技术,并根据 GNU 通用公共许可证授权。要在您本地的 Windows Live Messenger 上成功测试生成的 DLL,您需要:

  1. 在 Visual Studio 中打开 wlmplugindemo 解决方案。
  2. 以 Debug 或 Release 模式构建解决方案,两者均可工作。
  3. 在输出目录中,您会看到三个生成的 DLL。关闭系统中任何 msnmsgr.exe 进程,并将这三个 DLL 复制到 Windows Live Messenger 的应用程序目录中。

只要您遵守 GPL,就可以随意修改代码和/或将其作为您自己想法的基础。

如何快速开始使用预编译的 DLL

您可能希望使用本文附带的预构建库,而不是在 Visual Studio 中构建解决方案:只需关闭任何正在运行的 msnmsgr.exe,然后将这三个 DLL 复制到 Windows Live Messenger 目录中即可。

注意事项

本文附带的代码是为纯粹的研究而编写的;因此,许多优化和可靠的编码模式或实践可能被忽略了,唯一的目的是在短时间内取得成果。因此,这些代码必须仅被视为所阐述理论概念的实践实现,或作为逆向工程领域进一步研究的基础。

记住这个建议,让我们开始第一个主题。

进入 Live Messenger 地址空间

为了成功地、无限制地修改 WLM 功能,必须在 WLM 可执行文件的地址空间内运行我们的代码。有多种方法可以实现这一点,例如代理 DLL、远程线程注入、PE 补丁等。事实上,您可以在 CodeProject 上找到关于这些主题的优秀文章。

幸运的是,对于 WLM 可执行文件,我们可以使用一种称为代理 DLL 的简单技术成功注入我们的代码。这种方法的目标是创建一个“伪”DLL,它充当函数调用转发器,有效地位于目标进程和我们待注入的代码(通常是另一个 DLL)之间。只要我们的伪 DLL 的名称在目标可移植可执行文件 (PE) 的导入表中,我们的代理 DLL 就会被加载,从而允许我们在目标进程的虚拟地址空间中运行代码。

当然,我们不能简单地加载我们的注入 DLL,还需要欺骗目标可执行文件,让它相信它实际上执行的是“原始”DLL 而不是我们的伪版本:我们需要将调用转发到原始 DLL。只要我们能在目标可执行文件中找到一个只导出少量函数的导入 DLL,这并不难做到。对于 Windows Live Messenger,一个不错的选择是 MSIMG32.DLL,这是一个小型的 Windows 系统 DLL,实现了一些 GDI 函数,例如 GradientFill

Windows Live Messenger 的导入表

我们想要通过代理 DLL 的方法在 WLM 的地址空间中运行代码。从上面的解释可以清楚地看到,第一步是检查 WLM 可执行文件的 PE 导入表。一个检查 PE 导入和导出的优秀工具是 Dependency Walker。您可以从这里下载最新版本。

打开 Dependency Walker 并选择 Windows Live Messenger 可执行文件,会显示以下屏幕:

我们开始检查左侧窗格中每个导入的 DLL,查看所选 DLL 导出的函数数量。您可能会问,为什么不根据 WLM 使用的导入函数数量来选择 DLL,而是根据 DLL 的总导出函数数量。我们的方法更为保守,因为我们将在伪 DLL 中转发所有导出的函数;请记住,一般规则是转发目标可执行文件所需的所有调用。

当我们看到 MSIMG32.DLL 时,我们发现这无疑是一个绝佳的候选者,只有五个导出函数:

我们选择了 MSIMG32.DLL 作为代理 DLL,它将驻留在 WLM 可执行文件目录中,迫使它加载我们的伪 DLL,而不是位于 Windows system 目录中的 MSIMG32.DLL。请注意,我们可以强制从 WLM 可执行文件目录加载我们的 DLL,因为此 DLL 在 PE 导入表中被引用,因此由 NT 加载程序加载,而不是由可执行文件本身加载。请记住,其他应用程序可能会使用 LoadLibrary 手动加载 DLL,要么使用默认的操作系统库搜索顺序,要么使用绝对路径覆盖它。

关于 NT 加载程序加载 DLL 时搜索的文件系统位置的详细信息可以在 MSDN 上找到:动态链接库搜索顺序。关于原生 Windows NT DLL 加载程序的深入描述,包括 DLL 编程的注意事项,可以在这里的各种帖子中找到。

实现细节

我们对代理 DLL 的实现在 wlmplugindemo 解决方案的 proxyDLL 项目中。当 DLL 附加时,代码的总体执行流程是:

  • 使用 LoadLibrary 从 Windows system 目录加载 MSIMG32.DLL 系统库。
  • 使用 GetProcAddress 函数获取待转发函数的地址。
  • 将每个 MSIMG32.DLL 函数的地址存储在函数指针变量中。
  • 使用 LoadLibrary 加载要注入的 DLL。如果成功,我们就在 msnmsgr.exe 的地址空间中成功注入了我们的 DLL 代码。
  • 使用 GetProcAddress 获取被注入 DLL 的初始化函数地址(在我们的演示中,名为 InitDLL)。
  • 通过函数指针跳转到被注入 DLL 的初始化代码。

请注意,在 DllMainDLL_PROCESS_ATTACH 分支中使用 LoadLibrary 可能会导致加载程序死锁。在这种情况下,我们不会导致 NT 加载程序产生循环引用,所以是安全的,但始终要小心您在 DllMain 中执行的代码。

回到实现。请记住,我们需要编写“存根”函数定义,唯一的任务是通过上面解释的指针将调用转发到真正的 DLL,因此我们必须尊重我们想要转发的函数的签名。对于 MSIMG32.DLL 的五个导出函数,在 Windows API 文档中搜索可以找到我们感兴趣的函数声明:

BOOL TransparentBlt (HDC hdcDest, int nXOriginDest,  int nYOriginDest,
                     int nWidthDest, int hHeightDest, HDC hdcSrc, 
                     int nXOriginSrc, int nYOriginSrc, int nWidthSrc, 
                     int nHeightSrc, UINT crTransparent);
BOOL AlphaBlend (HDC hdcDest, int nXOriginDest, int nYOriginDest,
                 int nWidthDest,  int nHeightDest, HDC hdcSrc,
                 int nXOriginSrc, int nYOriginSrc, int nWidthSrc, 
                 int nHeightSrc, BLENDFUNCTION blendFunction);
BOOL GradientFill (HDC hdc, PTRIVERTEX pVertex, ULONG dwNumVertex, 
                   PVOID pMesh, ULONG dwNumMesh, ULONG dwMode);

这三个函数是微软官方文档化的,很容易处理。但是,对于剩下的函数 DllInitializevSetDdrawflag 的参数和返回类型,由于它们没有被官方文档化,情况就不那么明朗了。

在 MSDN 上搜索 DllInitialize,我们可以找到该函数的描述,但很明显,MSIMG32.DLL 中的 DllInitialize 并非官方的 DllInitialize:MSDN 文档指出导出驱动程序必须提供 DllInitialize 例程(参见 MSDN 上的 DllInitialize)。我们这里处理的是一个内核模式驱动程序的 DLL 吗?绝对不是,因为内核驱动程序必须提供一个 DriverEntry 导出符号,而 MSIMG32.DLL 中并没有这个符号。

此时,我们必须解决定义剩余函数返回类型和参数的问题。让我们使用 DUMPBIN 工具来转储 MSIMG32.DLL 的导出表。输出如下:

dumpbin C:\WINDOWS\SYSTEM32\MSIMG32.dll /Exports
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file C:\WINDOWS\SYSTEM32\MSIMG32.dll

File Type: DLL

Section contains the following exports for MSIMG32.dll



00000000 characteristics
8025BC7 time date stamp Sun Apr 13 16:15:19 2008
0.00 version
1 ordinal base
5 number of functions
5 number of names

ordinal hint RVA      name

2    0 0000119B AlphaBlend = _AlphaBlend@44
3    1 0000110C DllInitialize = _DllInitialize@12
4    2 0000117F GradientFill = _GradientFill@24
5    3 000010F0 TransparentBlt = _TransparentBlt@44
1    4 000012C6 vSetDdrawflag = ?vSetDdrawflag@@YGXXZ (void __stdcall vSetDdrawflag(void))

Summary

1000 .data
1000 .reloc
1000 .rsrc
1000 .text

如果您查看链接器为每个函数生成的符号,您可以推断出每个函数接受的 32 位参数的数量以及使用的调用约定。每种名称修饰格式对应一种调用约定。根据 Visual C++ 链接器文档 名称修饰,所有函数都使用 __stdcall,这是 Windows API 使用的调用约定。此外,对我们的研究非常有趣的是,vSetDdrawFlag 是一个 C++ 函数。像 DUMPBIN 这样的工具可以为我们反修饰名称,给出我们需要的函数签名:

void vSetDdrawFlag(void);

现在,DllInitialize 是我们需要考虑的最后一个函数。_DllInitialize@12 的名称修饰说明 DllInitialize 的参数总共占用 12 字节。如果我们正确地假设,因为这是一个在 32 位环境中运行的 32 位 DLL,所有数据都按 4 字节边界对齐,那么 DllInitialize 从其调用者那里接收三个 32 位参数。这个函数的初步声明当然可以是:

UNKNOWN_RETURN_TYPE DllInitialize (DWORD p1, DWORD p2, DWORD p3);

我们还剩下返回类型——我们知道这是一个 __stdcall;因此,EAX 寄存器被用作返回值的占位符。对 DllInitialize 函数的快速反汇编检查显示,该函数实际上在通过 RET 返回之前,通过 AL 返回了一个值。下面的列表是使用 PEBrowse Professional 捕获的,这是一个优秀的可移植可执行文件剖析工具,可在开发者网站 SmidgeonSoft 免费获取。

现在,有了这些信息,我们可以写下 DllInitialize 函数的最终声明:

DWORD DllInitialize (DWORD p1, DWORD p2, DWORD p3);

在继续之前,让我们回顾一下我们的研究,看看在处理 DLL 中的函数时通常可以采取的另一种方法。让我们使用 DUMPBIN 的 /HEADERS 参数来检查 MSIMG32.DLL 的头文件。以下是转储的前几行,我们可以从中找到关键信息:

Microsoft Visual Studio 9.0\VC>dumpbin C:\WINDOWS\SYSTEM32\MSIMG32.dll /headers
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file C:\WINDOWS\SYSTEM32\MSIMG32.dll

PE signature found

File Type: DLL

FILE HEADER VALUES
            ...
            ...
OPTIONAL HEADER VALUES
10B magic # (PE32)
7.10 linker version
600 size of code
600 size of initialized data
200 size of uninitialized data
110C entry point (7633110C) _DllInitialize@12
1000 base of code
2000 base of data
76330000 image base (76330000 to 76334FFF)
1000 section alignment
            ...
(output follows)

DllInitialize 是 DLL 的入口点,默认情况下是 DllMain,但可以更改。根据 MSDN 的说明,DLL 入口点例程具有以下原型:

BOOL WINAPI DllMain( __in  HINSTANCE hinstDLL,  
     __in  DWORD fdwReason, __in  LPVOID lpvReserved);

这个声明与我们之前的结果相匹配,我们确定了三个参数和一个非 void 的返回类型,所以我们之前对 DllInitialize 的定义是正确的(事实上,从微处理器的角度来看,它们是完全相同的),但为了更精确,我们最终将我们的声明重写为:

BOOL WINAPI DllInitialize(HINSTANCE p1, DWORD p2, LPVOID p3);

由于除了“喂给”我们的 MSIMG32.DLL 函数指针外,我们不会使用这些参数,所以我们如上所示命名了这些参数。

至此,我们对 MSIMG32.DLL 的分析结束了。现在,我们终于可以像下面这样声明我们的函数指针 typedef 和“存根”原型了:

//
// Function pointer typedefs for exports we are forwarding
//
typedef BOOL (WINAPI *PFNTRANSPARENTBLT) (HDC,int,int,int,int,HDC,int,int,int,int,UINT);
typedef VOID (WINAPI *PFNVSETDDRAWFLAG)  (VOID);
typedef BOOL (WINAPI *PFNALPHABLEND)     (HDC,int,int,int,int,HDC,int,int,
                                          int,int,BLENDFUNCTION);
typedef BOOL (WINAPI *PFNGRADIENTFILL)   (HDC,PTRIVERTEX,ULONG,PVOID,ULONG,ULONG);
typedef BOOL (WINAPI *PFNDLLINITIALIZE)  (HINSTANCE, DWORD, LPVOID);

// Function pointer typedef to injected-DLL initialization
typedef void (*PFNINITDLL) (void);

#ifdef __cplusplus
extern "C"
{
#endif

//
// Function protoypes
//
BOOL WINAPI TransparentBlt(HDC, int, int, int, int, HDC, int, int, int, int, UINT);
BOOL WINAPI AlphaBlend    (HDC, int , int, int, int, HDC, int , 
                           int, int, int, BLENDFUNCTION);
BOOL WINAPI GradientFill  (HDC, PTRIVERTEX, ULONG, PVOID, ULONG, ULONG);
BOOL WINAPI DllInitialize (HINSTANCE,DWORD,LPVOID);
VOID WINAPI vSetDdrawflag (VOID);

#ifdef __cplusplus
}
#endif // __CPLUSPLUS

上面的代码段位于 proxyDLL 项目的 msimgproxy.h 头文件中。请注意,我们使用 WINAPI 调用约定(__stdcall 的别名)导出了所有函数,并使用 extern "C" 导出了未修饰的名称,即使是 C++ 函数 vSetDdrawflag 也是如此——然而,这个函数是 void 且无参数的,所以我们可以安全地将其导出为 __stdcall 和未修饰的名称。

成功构建我们的代理 DLL 所需的唯一剩余文件是 DEF 文件,它非常简单:

LIBRARY    "msimg32"
EXPORTS
    vSetDdrawflag    @1
    AlphaBlend    @2
    DllInitialize    PRIVATE
    GradientFill    @4
    TransparentBlt    @5

在这个 DEF 文件中,我们遵循了真实 MSIMG32 库的序号。可选的关键字 PRIVATE 可以防止入口名称被放入 LINK 生成的导入库中。

最后,拦截 Windows Live Messenger 对我们伪 DLL 的调用,并将它们路由到位于 Windows System 目录中的原始 MSIMG32.DLL 的重要任务,只需使用我们先前获得的指向真实库的函数指针即可完成。以下代码展示了 AlphaBlend 的转发存根:

BOOL WINAPI AlphaBlend(HDC p1, int p2, int p3, int p4, 
                            int p5, HDC p6, int p7, int p8, int p9, 
                            int p10, BLENDFUNCTION dw)
{
      return pfnAlphaBlend (p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,dw);
}

至此,我们关于 MSIMG32 代理 DLL 研究与实现的论述结束了。

Windows Live Messenger 破解

代理 DLL 构建好并准备就绪后,我们就可以开始开发一些修改 WLM 功能的技术了。我们将描述如何实现以下目标:

  • 向 WLM 主窗口添加菜单
  • 向 WLM 窗口添加工具栏按钮
  • 获取联系人列表选择
  • 获取所选联系人的信息

在此之前,让我们简要但必要地描述一下来自 Nektra 的 API hook 库 Trappola

Trappola Hook 简介

TrappolaNektra 出品的一款优秀的免费 API Hook 库,可用于构建强大的 API 拦截应用程序(例如同样来自 NektraSpyStudio)。为了解释 Trappola,我们将看看它在这个项目中是如何实际使用的,这样您就可以在阅读本文时结合代码更好地理解这个优秀的拦截工具的概念和用法。请记住,本项目并未使用 Trappola 的所有功能。要深入了解,请随意自行探索和使用 Trappola。

定义 Hook 的内容、位置和方式

如果你查看 wlmplugDLL 项目中的 hooking.h 头文件,你会发现 HookArray[],这是一个 HOOK_DESCRIPTOR 结构体数组。顾名思义,每个 HOOK_DESCRIPTOR 定义了 Trappola 将要执行的特定拦截的属性。HOOK_DESCRIPTOR 结构体的成员及其含义总结在下表中。

结构体成员 含义
szTargetModule 要拦截的函数所在的模块(例如,USER32.DLL)。
szTargetFunction 我们想要在 szTargetModule 中拦截的函数(例如,CreateWindowEx)。
cbParam 传递给函数的参数总字节数,在 32 位环境中为 4 字节乘以参数数量。需要强调的一点是,所有参数都必须指定,甚至包括隐式参数,例如:C++ 成员函数中的 this 指针(当然,静态成员除外),它们通过 ECX 寄存器静默传递。
pvHookHandler 指向 hook 处理函数入口地址的指针。该处理函数的执行时机由 flags 成员定义。
nccCallingCnv 一个来自 NktCallingConvention 枚举的值,指定了被拦截函数的调用约定。可能的值请参见 Trappola 头文件 enums.h。请记住,大多数标准 Windows API 是 stdcall,而较新版本的组件可能会有所不同(C++ 或 COM 接口导出)。
iFlags 一组定义拦截模式的标志。在这个项目中,使用 _call_after 就足够了(hook 处理程序在被拦截的函数完成后执行,因此可以检查返回值)。_call_before 的语义类似。请查看 Trappola 源代码以了解可能的值。

此时,您应该能够理解 HookArray[] 数组中的每一个条目了。在下一节中,我们将 Hook CreateWindowEx 来向 WLM 的顶层窗口添加一个菜单项。

Hook 的附加和分离

当一个 NktApiHook 对象以两个 NktFunctionWrapper 函数为参数构造时,一个 hook 就变得活跃了。NktFunctionWrapper 对象封装了 hook 处理函数和要被拦截的函数。最终生成的 NktApiHook 对象被插入到一个包含 ApiHookPtr 对象的 std::list 中。而 ApiHookPtr 指针是来自 Yasper 库的引用计数指针(又称“智能指针”),该库可在 Sourceforge 上找到。

在我们的项目代码中,您可以看到在 HookArray[] 中定义的 hook 是如何被“激活”的,具体来说是在 hooking.cpp 文件中定义的 AttachHookAttachHookArray 函数中。

断开一个 hook 只需要从 hook 列表(在本项目代码中是全局变量 g_HookList)中移除相应的 NktApiHook 对象。当我们的插件 DLL 因操作系统调用 dllentry.cpp 中的 DLL_DETACH_PROCESS 分支而被卸载时,所有 hook 都会通过一句简单的 g_HookList.clear() 语句被移除。

有了对 Trappola 的这个概述,我们准备开始对 Windows Live Messenger 的第一次破解:通过子类化添加菜单。您可能认为 WLM 中的菜单是标准的 Win32 菜单资源,您是对的,但这是一个例外,因为 WLM 的大多数控件都不是标准的,而是自绘的。而且,不仅仅是自绘,而是用一种名为 DirectUI 的自定义 UI 绘制的,这完全没有文档。DirectUI 界面控件的绘图表面包含在它自己的窗口类中,该窗口类是菜单所在的顶层窗口类的子窗口。因此,下面详细介绍了 Windows Live Messenger 应用程序中涉及的窗口类。

Windows Live Messenger 窗口类

任何对 Windows Live Messenger 逆向和/或破解感兴趣的程序员可能会遇到的第一个令人惊讶的事情是,Windows Live Messenger 的客户区并不暴露任何子控件,而是一个独特的“表面”,所有控件似乎都绘制在这个表面上。让我们用 Spy++ 来看一下;不过,您也可以使用任何不错的 Win32 窗口监视工具,比如 Winspector

在上面的截图中,在同步的左侧窗格中,很明显 DirectUIHWND 窗口类不包含任何子控件,至少没有任何 Windows API 的控件。那么,这些花哨的 UI 元素是如何出现在 WLM 客户区的呢?通过 DirectUI,这是一个未文档化的框架,似乎类似于 XAML,即 Windows Presentation Foundation 中使用的基于 XML 的 UI 描述格式。在本文的资源 Hook 部分,我们将看到 DirectUI 使用的许多 XML 元素与 XAML 标签相似。无论如何,我们不打算直接执行 DirectUI 函数,而是修改由 DirectUI 解析器读取的资源。

如果您感兴趣,DirectUI 的功能包含在 Windows Live Messenger 应用程序目录下的 msncore.dll 文件中。在那里,使用任何 PE 导出工具(如 DependencyWalker 或 PE-Browse)都可以找到许多与这个未文档化的 UI 框架相关的 C++ 导出函数。当然,对这段代码进行逆向分析会非常有趣,尽管适用性有限,因为未来版本的 Windows Live Messenger 很可能会基于 WPF 代码,正如有关最新 9.0 测试版的网络搜索所表明的那样。

顶层窗口类名为 MSBLWindowClass,我们将在这里修改菜单。

修改 Windows Live Messenger 菜单栏

在本节中,我们的目标是解释如何使用 API Hook 修改 WLM 菜单栏。

Windows Live Messenger 不会动态创建应用程序菜单栏,而是从一个单独的 DLL(msgslang.8.5.XXXX.XXXX.dll)中存储的资源加载它们。您可以使用资源编辑器(如 Resource Hacker)来确认这一点,如下面的屏幕截图所示:

请注意,您的资源语言标识符,在 Win32 API 中称为 LANGID,可能会有所不同。在此截图中,3082 是西班牙语的 LANGID。

为了进行菜单修改,很明显主窗口必须已经成功创建。因此,一个极佳的拦截函数候选者是 CreateWindowEx。在这里,在 Hook API 之前,有必要澄清一些关于 ANSI 和 Unicode 函数的概念,因为字面上 Hook CreateWindowEx 是行不通的。而且,这适用于大多数 Win32 API 函数。

关于 ANSI 和 Unicode API

在 Windows 9X 中,所有操作系统的 API 字符串都是 ANSI,即 8 位字符字符串。当使用非英文字符(例如亚洲语言)时,256 个字符的限制就显而易见了。为了给应用程序和系统的国际化提供坚实和透明的支持,微软在 NT 内核系列中引入了每个字符 16 位的 Unicode 字符集,也称为宽字符串。Unicode 是基于 NT 的操作系统中唯一的标准字符串编码:Windows API 中对字符串的所有操作都是以 Unicode 完成的,因此 ANSI 字符串会先被转换为宽字符字符串。

为了与 Win16 和 Win9X 应用程序兼容,NT 还为几乎所有接受字符串参数的 API 函数引入了不同的版本(也有例外,比如 GetProcAddress,因为 PE 可执行文件中的所有表始终使用 ASCIIZ 编码)。我们之前谈到了 CreateWindowEx 函数;这个调用期望接收 ANSI 还是 Unicode 字符串作为输入?答案在于 Windows API 头文件宏:

#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else 
#define CreateWindowEx CreateWindowExA
#endif

那么,如果一个程序被编译为 Unicode,调用就是函数名加上后缀 'W',如果程序被编译为使用 ANSI 字符串,则后缀为 'A'。事实是,Unicode 软件只能在基于 NT 的系统上运行,而 ANSI 软件可以在所有 Win32 平台上运行。然而,如果你想开发能在所有 Win32 平台上运行的应用程序,你必须使用 TCHAR* 作为你的字符串类型:Windows 头文件宏会将你的 TCHARs 翻译成 wchar_t* (Unicode) 字符串,或者在你编译为 ANSI 时翻译成 char*。微软的文档在这个主题上非常清晰,你可以搜索它来获得关于基于 Unicode 和 ANSI 的应用程序的详细解释。

我们的情况明确而清晰:Windows Live Messenger 的代码是 100% Unicode。基于这一事实,我们将 Hook CreateWindowsExW,因为这是 USER32.DLL 库导出的真实名称。

拦截和子类化

正如我们在 Trappola 概述部分所见,一个 HOOK_DESCRIPTOR 类型的条目需要存在于 hooking.hHookArray[] 中。所需的描述符条目如下:

L"user32.dll", "CreateWindowExW", NUM_PARAMS(12), 
               Handle_CreateWindowExW, stdcall_, _call_after

NUM_PARAMS(x) 宏简单地将 12 乘以当前平台指针的大小。指定了上述条目后,可以使用 DECLARE_HOOK 宏或手动创建处理函数的原型。现在,只需记住 Trappola 函数处理程序所需的签名为:

void fn_Handler (NktHandlerParams* hp);

现在不必担心 NktHandlerParams,这个结构的实际用法将在本文中逐步揭示。关于 hook 描述符的最后一个必要说明是,为了成功拦截 C++ 函数,必须使用修饰后的名称。因此,如果你想 hook 的模块导出了一个 C++ 函数,例如,名称为 Initialize@MyClass@@XYZXYZClass1@Object1@@@Z,你应该指定这个字符串作为目标函数来 hook,而不是像 DependencyWalker 这样的工具可能显示的未修饰名称(例如,对于给定的例子是 MyClass::Initialize(Class1::Object1*))。

我们创建菜单的方法是拦截 CreateWindowExW。如果请求的窗口类对应于 Live Messenger 的顶层窗口(MSBLWindowClass),我们就子类化已创建的窗口,以更改其窗口过程,这样我们就可以处理我们想要的消息,特别是 WM_SHOWWINDOW。你可能想了解一下窗口子类化,MSDN 页面 使用窗口过程 解释了你需要知道的一切。

那么,让我们动手来写 wndhook.cpp 中的 hook 处理程序代码吧:

void Handle_CreateWindowExW (NktHandlerParams * hp)
{
    LPCWSTR lpszWndClass = *(LPCWSTR*) PARAMETER_INDEX(1);
    if (lpszWndClass && HIWORD((INT_PTR )lpszWndClass) )
        if (wcscmp(lpszWndClass, g_wlmTopWindow.szWndClass) == 0)
        {
            g_wlmTopWindow.hwnd = (HWND)hp->context.regs->EAX;
            g_wlmTopWindow.pfnOldWndProc = (WNDPROC)
                               GetWindowLongPtr (g_wlmTopWindow.hwnd, GWLP_WNDPROC);
            SetWindowLongPtr (g_wlmTopWindow.hwnd, GWLP_WNDPROC, (LONG_PTR)WlmWndProc);
        }
}

上面的代码段有很多需要解释的地方,所以让我们逐行分析。

  • hooking.h 中定义的 PARAMETER_INDEX(P) 宏返回函数处理程序中位置为 P 的参数值。预处理器会将 PARAMETER_INDEX(p) 宏展开为 ((size_t)hp->context.pms + (sizeof(DWORD) * (x)));因此 hp->context.pms 返回一个包含函数参数值的 32 位值数组——相应地,在 hp->context.pms+32*N 指向的地址处,你可以找到 32 位系统中的第 N 个参数值(在撰写本文时,Trappola 尚不支持 64 位平台)。正如你对这样一个通用函数所期望的,PARAMETER_INDEX 返回 void*,所以需要进行适当的类型转换。
  • 在所示的处理程序中,我们需要 CreateWindowEx 的第二个参数(索引为 1),它是一个 LPCWSTR 字符串类型,表示要创建的窗口的类名。

  • 第一个 if {} 块检查了 Windows API 中发现的众多怪异现象之一。通常你可能期望一个类名是一个字符串,但事实并非如此。根据微软文档,传递给 CreateWindowEx 的类名参数可以是一个 ANSI 或 Unicode 字符串指针,指定一个字面类名,也可以是一个 ATOM 值。ATOM 指定一个映射到字面字符串(在本例中是类名)的值,但它不是一个字符串。事实上,ATOM 是一个 16 位的值,所以高位字(HIWORD)为零。如果你不检查这一点,你可能会拦截到一个以 ATOM 为参数的窗口创建——将此解释为字符串指针会导致程序崩溃,因为 0x0000-0xffff 的地址范围对任何用户模式进程都是不可访问的。因此,根据我们的分析,只有当被拦截的 CreateWindowEx 的窗口类名参数代表一个有效的字符串指针地址时,if 块才会执行。
  • 内部 if 语句中的比较,如果正在创建的窗口是 WLM 的顶层窗口,则返回 true,该窗口的类名存储在全局标识符 g_topWlmWindow.szClassName 中。这个变量是 WLM_TOPWINDOW 结构的一个实例,用于存储关于这个窗口的值,如类名和句柄。
  • CreateWindowEx 成功创建一个窗口时,它会返回该窗口的窗口句柄 (HWND) 值。我们将这个值存储在 g_topWlmWindow.hwnd 中,从函数 hook 处理程序内部的实际返回值中获取它。这怎么可能呢?请记住,这个函数是用 _call_after 标志进行 hook 的,因此当 Handle_CreateWindowExW 开始执行时,CreateWindowExW 已经返回了一个值;并且因为这是一个 Windows API 调用,返回值是通过 EAX 寄存器传递给调用者的。
  • 你可以看到这是 NktHandlerParamscontext 成员的另一个用法。hp->context->regs 让你有可能访问 x86 寄存器。例如:如果你正在 Hook 一个 C++ 非静态类成员函数,hp->context->regs.ECX 可能会给你 this 指针的地址。

  • 最后,通过最后两行的 GetWindowLongPtr/SetWindowLongPtr 对,我们通过指定我们自己的窗口过程来子类化 Windows Live Messenger 的顶层窗口。当然,旧的窗口过程地址必须保存下来,因为如果我们的窗口过程不处理某个特定的消息,它应该让原始的窗口过程来处理它。这种技术允许以一种伪面向对象的方式实现“可扩展的”窗口过程,你可以只重写你想要的消息处理,让“父”(旧)过程来处理其余的。

因此,当顶层窗口的 CreateWindowExW 被调用时,我们的 hook 处理程序会存储 HWND 以备后用,并对窗口进行子类化。我们在子类化窗口过程中要做的是,当窗口显示时,向菜单栏添加一个元素,并且最好是一次性永久添加。

我们必须检查要处理哪个窗口消息来完成这个任务。WM_SHOWWINDOW 实际上是可行的,因为在 Windows Live Messenger 中,它在窗口第一次显示时只被调用一次(此行为取决于某些条件,更多信息请参见 WM_SHOWWINDOW 通知)。添加菜单元素的代码如下:

case WM_SHOWWINDOW:

    hMainMenu = GetMenu(hwnd);
    hDemoMenu1 = CreatePopupMenu();

    AppendMenu(hMainMenu, MF_STRING | MF_POPUP, (UINT_PTR) hDemoMenu1, 
               L"W&lmPluginDLL");
    AppendMenu(hDemoMenu1, MF_STRING, ID_MENU_DISPLAY_CONTACTINFO, 
               L"Display Contact Information &Window");
    AppendMenu(hDemoMenu1, MF_STRING, ID_MENU_DEMO_ABOUT, L"About...");
    DrawMenuBar(hwnd);

我们使用 GetMenu 从顶层菜单获取菜单句柄,使用 CreatePopupMenu 创建一个新的弹出菜单,然后通过这个句柄追加菜单项。

剩下的任务是捕获 WM_COMMAND 来处理我们的菜单点击。这是以标准的 Win32 API 方式完成的。代码骨架可能如下所示:

case WM_COMMAND:
    // check if it's from our menu
    if (HIWORD(wParam) == 0)
    {
        switch (LOWORD(wParam))
        {
        case ID_MENU_DEMO_ABOUT:
        ...
        ...
        break;

        case ID_MENU_DISPLAY_CONTACTINFO:
        ...
        ...
        break;
        }
    }

请记住,您必须将未处理的消息返回给原始的窗口过程。这就是为什么我们将旧的窗口过程地址保存在全局变量 g_wlmTopWindow.pfnOldWndProc 中的原因。对于旧的窗口过程,Windows API 提供了 CallWindowProc 函数,因此窗口过程 switch..case 作用域中消息处理的默认情况应该是:

return CallWindowProc (wndProc, hWnd, uMsg, wParam, lParam);

在我们构建的子类化窗口过程中,通过将第一个参数替换为 g_wlmTopWindow.pfnOldWndProc 来设置旧的窗口过程函数指针。您可以在 wndhook.cpp 源文件中查看详细信息。

这是我们的结果:

如你所见,我们已经通过 Hook 和子类化成功地添加了一个可用的菜单。在接下来的部分,我们将解释如何通过子类化实现“关于”和“联系人信息”窗口。

“着色”窗口类:子类化实践

如果您的好奇心战胜了您,并且已经成功测试了该项目,您可能会注意到“关于”窗口和“联系人信息”窗口在视觉和行为方面是相似的,如下面的截图所示:

这两个窗口都基于同一个窗口类,即“Colorized”窗口类,定义为 NKT_COLORIZED_WINDOW_CLASS。这个名字揭示了基于该类的窗口所继承的属性之一:窗口通过从 WLM 顶层窗口客户区采样一个像素颜色来进行“着色”,此外还使用圆角矩形区域获得圆角形状。请记住,我们不会提供所有细节,例如窗口背景的渐变填充是如何构建的,而是提供这种方法的总体结构。

窗口类允许将属性和功能以一种*形式上*的继承方式传递给子类化的窗口。子类化的窗口可以重写、接受或忽略属于默认窗口类过程的窗口消息。这适用于大多数在 Win32 API 中被归类为“窗口”的 UI 元素,例如:编辑框。验证编辑控件文本的一种典型方法是在用户输入时通过子类化编辑控件来实现。然而,我们想强调这个非常重要的一点:*窗口子类化不是面向对象编程(OOP)的继承和/或多态*。它们在*概念上相似*,但窗口子类化是一种 Windows 编程技术,而不是特定编程范式的实现或应用。澄清了这个潜在的混淆之后,让我们继续。

打开源文件 clrzwind.cpp 并查看其窗口过程,可以清楚地看到基于该类的窗口默认会处理以下消息:WM_CREATEWM_ERASEBKGNDWM_DESTROYWM_NCHITTEST 以及应用程序定义的 WM_UPDATEBASECOLOR

当窗口类通过 RegisterClassExRegisterColorizedWndClass 函数中注册时,你可以看到所有窗口都会使用类样式 CS_DROPSHOW 来投下阴影;没有定义背景,所以 WM_ERASEBKGND 的代码会绘制背景,并且通过 WNDCLASSEX 结构的 cbClsExtra 成员为窗口类定义了一些额外的字节。在许多 Windows API 应用程序中,你会发现这个成员被设置为零。但请记住,我们希望所有基于这个类的窗口共享一个共同的背景色,所以这个视觉方面是依赖于类的,而不是依赖于窗口的。我们将把这个颜色信息存储在窗口类本身中,而不是使用全局变量:这就是我们保留存储 RGBA 颜色所需字节数(即一个 COLORREF 的大小)的原因。关于类额外字节的最后一点是,最多可以分配 40 个字节,所以经验法则是,如果你想引用更大的数据结构,就存储一个指针。

现在,两个窗口(“关于”和“联系人信息”)都以相同的方式创建。它们定义了自己需要的特定于窗口的属性,但指定了 NKT_COLORIZED_WINDOW_CLASS 作为它们的类:

void CreateAboutDlg(HWND* pHWnd) 
{
    *pHWnd = CreateWindowEx(NULL, wszColorizedWndClass, NULL, WS_POPUP, 
        CW_USEDEFAULT, CW_USEDEFAULT, CX_DIALOGSIZE, CY_DIALOGSIZE,    
        NULL, NULL, hDllInst, NULL );

    // subclass
    if (*pHWnd)
    {
        pfnColorizedWndProc = (WNDPROC) GetWindowLongPtr(*pHWnd, GWLP_WNDPROC);
        SetWindowLongPtr(*pHWnd, GWLP_WNDPROC, (LONG_PTR)AboutDlgProc);
    }
}
void CreateContactInfoWnd(HWND* pHWnd)
{
    // Create the contact info window with the colorized window class

    *pHWnd = CreateWindowEx(WS_EX_NOACTIVATE, wszColorizedWndClass, NULL, WS_POPUP, 
        0, 0, CIW_DEFAULT_WIDTH, CIW_DEFAULT_HEIGHT, 
        NULL, NULL, hDllInst, NULL );

    // window subclassing
    if (*pHWnd)
    {
        pfnColorizedWndProc = (WNDPROC) GetWindowLongPtr(*pHWnd, GWLP_WNDPROC);
        SetWindowLongPtr(*pHWnd, GWLP_WNDPROC, (LONG_PTR)ContactInfoWndProc );
    }
}

对这两个窗口进行子类化的技术与我们之前看到的一样:我们将每个子类化窗口的窗口过程更改为一个专门的过程,在那里我们可以重写由“父”类窗口过程处理的消息,处理新的消息,甚至“拒绝”消息(这与重写相同,但我们只是从窗口过程中返回而不做任何事情)。

所涉及的子类化窗口过程的代码结构应该能澄清这一点。让我们先看看“关于”窗口的过程:

LRESULT AboutDlgProc (HWND hwnd, UINT uMsg, WPARAM wparam, LPARAM lparam)
{
  switch(uMsg)
  { 
  case WM_CREATECHILDCTRLS:
    if (!fChildCreated)
    {
      //
      // Create child control (OK button) and load bitmap
      //
      break;
    }

  case WM_COMMAND:
    if (LOWORD(wparam) == ID_OK)
      // Handle OK button press
    break;

  case WM_PAINT:
  {
      //
      // Paint logo and text
      //
      return 0L;      
  }
  
  case WM_CLOSE:
    //
    // Inhibit WM_DESTROY!
    //
    
    return 0L;
  }

  return CallWindowProc(pfnColorizedWndProc, hwnd, uMsg, wparam, lparam);
}

让我们分析一下这个过程。WM_CREATECHILDCTRLS 消息是由我们的应用程序定义的(作为 WM_USER+100),用于创建子控件和加载 logo 位图;你可能会问为什么我们不在 Windows 向回调函数发送 WM_CREATE 消息时直接完成这些任务。原因是你不能。当“关于”窗口被创建时,它被定义为基于 Colorized 窗口类——这意味着只有 Colorized 窗口过程的 WM_CREATE 消息会被调用,因为你是在窗口创建*之后*进行子类化的。然而,可以在窗口创建之前使用 SetClassLongPtr 来更改 Colorized 窗口类的窗口过程,尽管不推荐这样做,因为它可能会影响基于该类的其余窗口。

所以,根据我们的代码,在创建“关于”窗口后,我们可以调用 SendMessage 并附带 WM_CREATECHILDCTRLS 来设置我们的窗口。WM_COMMANDWM_PAINT 被我们的窗口“特化”了,因为 Colorized 窗口过程将这些消息传递给了默认的 Windows 处理调用 DefWindowProc。这里有趣的消息是 WM_CLOSE:这个消息在我们的“父”窗口类的窗口过程中也被传递给了 DefWindowProc,这意味着,默认情况下,当例如在这个窗口激活时按下 Alt+F4,Windows 会调用 DestroyWindow。我们不希望发生这种情况,因为如果窗口被销毁,我们必须再次创建它,这是不必要的;因此,我们修改了 WM_CLOSE 处理的代码,以“模拟”点击 OK 按钮,这实际上会隐藏窗口。我们通过一个简单的 SendMessage 调用来做到这一点:

SendMessage (hwnd, WM_COMMAND, MAKELPARAM(ID_OK, 0), 0);

与往常一样,对于其余的消息,我们使用 CallWindowProc 函数将它们传递给 Colorized 窗口类的过程。注意不要将它们传递给 DefWindowProc:这是不正确的,因为基类的任何消息都不会被调用。

现在,为了结束我们关于窗口子类化的讨论,需要注意的是,基于 Colorized 类的窗口可以从客户区拖动,因为它们没有标题栏,这是通过 WM_NCHITTEST 消息实现的。但是,我们不希望联系人信息窗口是可移动的,因为它“附着”在主 Windows Live Messenger 窗口上。我们如何实现这一点呢?只需在联系人信息窗口过程中“拒绝” WM_NCHITTEST 消息即可,代码如下:

case WM_NCHITTEST:    // prevent dragging
return 0L;

通过这个总体概述,您可以在源代码中查看细节;相关文件是 clrzwind.cppaboutdlg.cppcinfownd.cpp

Windows Live Messenger 资源简介

在本节中,我们将探讨 Windows Live Messenger 内部最有趣的一个方面:应用程序资源。首先,我们将介绍 WLM 资源的存储位置和方式,然后介绍一个资源管理器的实现,该实现允许,例如,向 Windows Live Messenger 工具栏添加按钮。

资源存储在哪里

Windows Live Messenger 使用的资源可以分类为:

  • 标准的 Win32 应用程序资源,例如对话框、菜单、图标、位图、字符串等,您可以使用 C API 或 MFC 等方式将其插入到您的原生 Win32 应用程序中。
  • 由内部 DirectUI 框架管理并由 XML 描述的资源。

第一种资源位于 msgslang.8.x.xxxx.xxxx.dll 中,并且如 DLL 文件名所示,是依赖于语言的。第二种资源可以在 msgsres 动态链接库中找到。我们说过这些资源以 XML 格式存储,您可以使用 Resource Hacker 等工具来验证这一点;您会发现其中包含许多资源类型,大多数由一个整数标识。为了开始我们的检查,一个有趣的资源类型是 #4004,其中找到了大多数基于 Windows Live Messenger Direct-UI 的对话框。下面的屏幕截图显示了一个 Resource Hacker 会话,展示了“Toast 窗口”的 XML 描述,这是当消息到达、联系人上线、收件箱有新邮件等情况下从系统托盘弹出的一个小弹出框。

感谢 MSN-modders 论坛 mess.be 的研究,我们可以列出资源 DLL 中的资源类型列表,以及每个资源 ID 条目对应的界面元素。论坛的原始链接在这里

ID 描述
919 MSN 今日焦点、选项卡内容、待处理请求
920 对话窗口
921 信息提示窗口(Toast Window)
922 群组对话容器
923 联系人列表
926 标准消息框
930 活动菜单、我的游戏
931 联系人卡片
932 活动菜单,我的活动
934 表情符号对话框
935 Winks 对话框
937 下载 MSN Messenger 内容,“单击对话框”
940 内容下拉菜单
941 动态显示图片容器
942 自动后台共享对话框
943 连接测试对话框
944 Flash 升级对话框
945 墨迹、视频、发送文件和呼叫下拉菜单
946 背景对话框
947 登录窗口
949 文件共享窗口
950 文件共享历史记录
952 与信息提示(toasts)相关的模板(需要进一步研究)
953 危险文件传输对话框
1001 地址簿工具栏(带有字轮、添加联系人和管理联系人按钮的那个)
1004 详细联系人列表
1009 普通联系人列表
44101 联系人选择器对话框
44102 Well 地址模板
44103 更多与字轮相关的元素
44104 地址簿模板 (好友, 好友选择器展开, 群组, 错误)
45700 移动选择器
45701 编辑群组对话框
45703 人员选择器
45704 额外的地址簿模板
45705 移动地址选择器
45710 添加/编辑联系人对话框

此外,在资源类型 #4005 中,可以找到与每个 #4004 资源相匹配的“样式”。我们可以将其类比为 (X)HTML+CSS:前者定义文档内容,而 CSS 定义视觉属性——通过这个类比,类型为 #4004 的 XML 资源描述了每个 UI 元素的大致内容,而位于资源树中 #4005 资源类型下的相同资源 ID 则指定了匹配元素的视觉和行为方面。需要考虑的一个重要问题是,据我们所知,XML 元素是由 Windows Live Messenger 内部检查的,因为在我们检查的 DLL 中,没有用于验证资源的 XML schema。感谢许多热情程序员对 DirectUI XML 格式内部的逆向工程,现在有一个 XSL 样式表可用,其中定义了所有版本 8.5 的元素。这个非常有用的文件可以在 MemSkin+ 工具网站上找到。

考虑到所有这些,我们需要看看需要 hook 哪些函数才能拦截 WLM 的资源。

资源函数调用顺序

Windows API 的四个基本资源管理函数,它们需要的参数,以及它们返回的值类型,都浓缩在下面的列表中:

函数 语义
HRSRC FindResource(HMODULE, LPCTSTR, LPCTSTR) 确定指定模块中具有指定类型和名称的资源的位置。
HGLOBAL LoadResource(HMODULE, HRSRC) 将指定的资源加载到全局内存中。
LPVOID LockResource(HGLOBAL) 锁定内存中指定的资源,返回指向该资源第一个字节的指针。
DWORD SizeofResource (HMODULE, HRSRC) 返回指定资源的字节大小。

查看上述函数的输入参数和输出返回值,很明显,应用程序要能够加载并使用资源,FindResource -> LoadResource -> LockResource 是成功获取有效资源指针所需的步骤。然而,资源的大小可以在这些调用之间的任何时候获取,但必须在 FindResource 返回有效的 HRSRC 句柄*之后*,正如 SizeofResource 的输入参数类型所表明的那样。这个在 Live Messenger 中经常使用的通用调用序列,可以用下图表示:

带着这个概览,我们的任务是正确地 hook 资源函数;记住,我们说过目标之一是向主 WLM 工具栏添加按钮,这可以通过 hook 那些 DirectUI XML 资源来实现,但具体用什么方式呢?

我们首先需要的是工具栏的 XML 定义。查看之前的资源列表,我们可以推断出工具栏应该位于联系人列表 XML 资源中:这个资源由 #4004 资源类型中的 ID #923 标识。使用 Resource Hacker 检查资源数据会得到一大块文本;然而,搜索文本“Toolbar”将引导我们找到工具栏定义的开始。这位于 <MToolbar>..</MToolbar> 标签对之间,代码量不大。下面展示了它以及两个按钮定义,以防您还没有自己探索过资源 DLL。

<MToolbar TextSuppressOption=Individual padding=rect(1,1,1,1) layoutpos=client>
<button active=mouse|nosyncfocus accessible=false 
    id=atom(ToolbarScrollLeft) class="HIGToolbarLeftScrollButton;" 
    tooltip=true/>
<button class="HIGToolbarButton" layout=borderlayout() 
    id=Atom(browsebarback) cmdid=40600 AccRole=43 
    Tooltip=true active=mouseandkeyboard|nosyncfocus Enabled=false>
<ButtonIcon class="HIGToolbarIcon" contentalign=middlecenter ID=Atom(ai13)/>
<element layoutpos=Right contentalign=middlecenter layout=filllayout()>
<ButtonText class="HIGToolbarText" contentalign=middlecenter Shortcut=1 ID=Atom(ai14)/>
</element>
</button>
...
...
...
<button class="HIGToolbarButton" layout=borderlayout() 
    id=Atom(browsebarhelp) cmdid=40608 AccRole=43 
    Tooltip=true active=mouseandkeyboard|nosyncfocus>
<ButtonIcon class="HIGToolbarIcon" contentalign=middlecenter ID=Atom(ai27)/>
<element layoutpos=Right contentalign=middlecenter layout=filllayout()>
<ButtonText class="HIGToolbarText" contentalign=middlecenter Shortcut=1 ID=Atom(ai28)/>
</element>
</button>
<button active=mouse|nosyncfocus accessible=false 
    id=atom(ToolbarScrollRight) class="HIGToolbarRightScrollButton" 
    tooltip=true/>
</MToolbar>

在每个 XML 资源定义中,id=atom(X) 与资源类型 #4005 中找到的样式表相关,其中一个匹配的 ID 为 x 的样式被应用到 UI 元素上。例如,我们在这里看到的第一个元素 ID 是 ToolbarScrollLeft,这很可能就是当工具栏元素数量超过渲染区域,并且用户向右滚动工具栏,隐藏了最左边的元素时,用于向左平移工具栏的按钮。这个滚动按钮的样式可以在资源 4005:923 中找到,其样式定义如下:

button[id=atom(ToolbarScrollLeft)]
{
    padding:rcrect(20213);
    FontSize:rcint(20074);
    accdesc:rcstr(20105);
    accname:rcstr(20106);
    Content:rcstr(20104);
}

UI 元素的属性可以直接指定,也可以通过 rcXXX(RID) 关键字来指示属性的定义在另一个资源中。例如,第一行定义了元素的内边距矩形,该定义位于资源 ID 20213 中。在 WLM 样式表资源中,您可以找到以下 rcxxx 引用,以及它所代表的资源种类和可以找到被引用资源 ID 的位置。

关键字 资源类型 Location
rcstr 字符串 位于依赖语言的 DLL msgslang.[version].dll 中的字符串表资源
rcimg 位图 (RLE/PNG) 资源类型 #4000
rcint 32 位整数值 资源类型 #4002
rcrect 矩形(15字节值) 资源类型 #4003
rcclr 32 位 RGBA 颜色 资源类型 #4002
rcbkd 背景 (RLE/PNG) 资源类型 #4000

位图以标准 PNG 格式存储,或者以一种尽管头字节显示“RLE”,但大多数声称能读取行程长度编码(Run-Length Encoding)位图的应用程序都无法读取的格式存储。这可能是微软使用的内部文件格式,因为唯一查看它们的方法是使用内部的 msncore.dll 函数。一个从 RLE 到 PNG 的转换器可以在这里找到。

资源管理类

回到 Windows API 资源管理函数及其拦截的话题,我们准备提出一个通用的资源管理类的方法。这个资源管理器的任务是跟踪要被 hook 的资源,为额外的资源数据分配和释放虚拟内存,并在运行时修改我们想要的资源。我们的实现是空间高效的,因为它只为插入到现有资源中的额外数据或为用户分配新资源而分配所需的内存;它还提供了一个 O(log N) 的执行上界,以最小化对目标 Windows Live Messenger 进程资源处理的性能影响。供您检查的代码位于 resmgr.hresmgr.cpp 中。

内部表

资源管理类(从现在起称为 RMC)维护三个基于 std::map 的表,用于跟踪资源 hook 活动:注册资源表、资源句柄表和资源指针表。所有表项都以键值对的形式存储,具体来说是 std::pair 对象。下面的列表列举了每个表的目的和大致结构。

  • 注册资源表(声明为 REGISTERED_RESOURCE_TABLE)是主表,所有我们希望由 RMC 管理的资源都存储在这里。每个条目的第一个键值元素是一个 ULONGLONG 值(64 位无符号),用于同时保存资源 ID 和类型。utility.h 中的 MAKEULONGLONG 宏用于从两个 32 位双字构建一个 64 位无符号值。第二个键值元素是 RESOURCE_MOD_DESCRIPTOR 结构的一个实例,它描述了这个资源条目的属性。该结构成员的含义将在稍后解释。
  • 第二个表称为资源句柄表(RESOURCE_HANDLE_TABLE),它存储由 FindResource API 返回的值(HRSRC 的值),以及一个 ULONGLONG 值,该值作为指向注册资源表中匹配条目的指针。这个表的目的是为 LoadResource 函数提供 HRSRC 句柄,以防该函数试图加载一个打算被 hook 的资源。如果是这种情况,并且 LoadResource 能成功返回一个 HGLOBAL,这个返回的句柄将存储在下一个表中。
  • 最后,第三个表,也是对我们需求最重要的表,是资源指针表(RESOURCE_POINTER_TABLE),其中存放着指向资源的内存地址。对于每个条目,该表包含一个键值对,其第一个元素是成功调用 LoadResource 返回的 HGLOBAL,第二个元素是一个子键值对,其第一个元素是代表已注册资源的 64 位无符号值,第二个元素是 LockResource 返回的资源地址。

一个关键概念是,我们的 Hook 工作试图尊重原始(未被 Hook)API 的语义,例如,LockResource 的处理程序是唯一处理内存地址的,你可以在其中获取未被 Hook 的资源地址,或指向新分配资源的地址。我们也试图最小化 API 之间的调用,尽量将它们隔离。这部分是由于 SizeofResource 函数可以在 FindResource 返回有效的 HRSRC 后的任何时间点被调用,正如我们在上一节中看到的那样。

RMC 的内部表及其一般操作可以在以下示意图中看到(数值是虚构的):

内置资源和外部资源

RMC 支持修改应用程序加载的任何 WLM 资源,或从外部 DLL 插入新资源。例如,你可以通过告诉 RMC 在 MToolbar 标签对中插入一个 XML 定义来插入一个工具栏按钮;然而,有趣的问题是在相关的样式表中使用 rcimg() 为新按钮选择一个位图。这个位图可以引用一个现有的位图资源,这不会造成大的问题。但是,使用我们自己的按钮图片呢?RMC 的方法是允许指定新的资源 ID,这些 ID 引用一个外部 DLL(resdll.dll)。总的想法是,FindResource 将尝试加载一个在 WLM 资源 DLL 中不存在的资源(因为我们指定了一个不存在的 ID);在这种情况下,如果这个请求确实引用了我们的资源,RMC 最终将返回一个有效地址,该地址指向的资源不在 WLM 资源 DLL 中,而是在我们自己的 DLL 中。

注册资源

就 RMC 而言,所有要操作的资源(无论是来自 WLM 资源 DLL 还是外部资源)都必须注册到 RMC 表中。此功能由资源管理类 CResourceManager 的两个成员函数提供:RegisterResourceRegisterNewResource。下面是第一个函数的原型以及每个参数的说明。

void CResourceManager::RegisterResource (DWORD dwType, DWORD dwName, LPVOID pvResData, 
                                         DWORD cbResDataSize, UINT iResModType, 
                                         UINT cbWhere = 0);
参数 用法
dwType, dwName 指定资源类型和名称 ID。这必须指向一个现有的 WLM 资源。
pvResData 指向要为指定资源添加、插入、追加或替换的资源数据的第一个字节的指针。
cbResDataSize pvResData 所指向数据的大小(以字节为单位)。
iResModType 一个符号常量,指定原始资源与 pvResData 所指向数据之间要执行的操作。有效常量如下:
RR_APPEND 数据被追加到原始资源的末尾。
RR_INSERT 数据被插入到资源中由 cbWhere 参数指定的字节位置处。
RR_REPLACE 原始资源被新数据完全替换。
RR_COPYRR_NONE 不对资源进行任何修改:原始资源数据被复制到一个新分配的虚拟地址。
cbWhere 插入位置之前的字节数。默认为零,且仅当在 iResModType 参数中指定了 RR_INSERT 时有效。

上述函数仅用于注册现有资源。要使用来自外部资源 DLL resdll.dll 的资源,RMC 提供了 RegisterNewResource 函数。

void CResourceManager::RegisterNewResource (DWORD dwType, DWORD dwName, 
                                            const RESOURCEINFO& ri);
参数 用法
dwType, dwName 指定资源类型和名称 ID。资源类型可以是,例如,4000 表示 WLM 位图,而名称 ID 可以是 WLM 资源中未使用的任何值(例如 > 60000)。
ri RESOURCEINFO 结构的引用,该结构指定了要使用的资源中包含的外部 DLL 的属性。RESOURCEINFO 结构有以下成员:
uResId 来自所提供 DLL 的资源 ID。
wszResType 资源的资源类型字符串。此类型参数可以使用 MAKEINTRESOURCE 宏转换为整数。
hrsrc FindResource 获取的 HRSRC 句柄。
dwSize SizeofResource 返回的资源大小(以字节为单位)。
hResData LoadResource 返回的数据句柄。
dwSize LockResource 返回的资源地址。

在下一节中,我们将展示 RMC 如何有效地管理资源;有关完整的类参考,请参阅源代码。

添加工具栏按钮示例

在本文的这一节中,将展示一个资源挂钩和管理的实际示例。您可以参考 toolbar.cppdllentry.cpp 来跟进此技术的阐述。

打开 dllentry.cpp 源文件,并滚动到下面出现的数组定义处:

WLM_TOOLBARBUTTON tbb[2];

我们将要创建两个额外的按钮;因此,我们定义了一个包含两个 WLM_TOOLBARBUTTON 结构的数组,该结构定义了按钮的外观和行为。WLM_TOOLBARBUTTON 包含在 wlmplugin.h 头文件中声明的以下成员:

WLM_TOOLBARBUTTON 成员 用法
fExtBitmap 如果为 true,按钮图像的位图来自外部 DLL,例如附带的 resdll.dll;否则,资源位于 Windows Live Messenger 资源文件 msgsres.dll 中的类型 ID #4000 处。
hModule 如果 fExtBitmaptrue,则为外部 DLL 的模块句柄;否则为 NULL
wszResType 资源 DLL 中的资源类型字符串,例如 "PNG"、"RLE"、"BITMAP"。提供额外位图的简单方法是将它们编码为 24 位 PNG,因为 DirectUI 代码可以毫无问题地加载和显示它们。
uResID 如果 fBitmapExttrue,则为外部 DLL 的资源名称 ID。如果资源位于 WLM 资源 DLL 中,uResID 值必须存在于资源名称 #4000 中。
uPosition 按钮在工具栏中的定位位置。可接受的值有:
  • 要修改的资源数据中的绝对字节位置。例如,0x1e05 将把按钮放在 Windows Live Messenger 8.5 的最后一个位置(请使用资源查看工具查看 msgsres.dll 资源 4004:923 的十六进制转储)。
  • 常量 TB_INSERT_FIRST,用于将按钮插入到工具栏的前端。
szButtonId 用于 UI XML 定义资源和 XML 样式表的唯一按钮 ID ANSI 字符串。
wszButtonID szButtonId 的宽字符字符串版本,用于需要 Unicode 字符串的函数,避免了转换函数。
szTooltip 当鼠标悬停在按钮上时显示的 ANSI 工具提示字符串。
pfnAct 指向按下按钮时要执行的函数的指针。与 PFNBUTTONPROC 类型兼容的函数必须符合以下签名:
void function_name (void *)
void* 参数是可选的,旨在传递用户定义的数据。

WLM_TOOLBARBUTTON 数组定义之后,我们用数据填充结构成员。正如您查看 dllentry.cpp 代码所能推断的,按钮具有以下属性:

  • 两个按钮都将插入到工具栏的前端。
  • 位图资源是外部的。这些资源的 ID 是 IDB_MPLAYERIDB_NKT
  • 当被按下时,它们分别执行 mplayrunwebnkt

其余的成员赋值都相当直观。源文件中的最后几行代码为我们在文章中多次谈到的 XML UI 资源和 XML 样式表分配了固定大小的缓冲区。AddToolbarButtons 函数接受 WLM_TOOLBARBUTTON 数组及其包含的元素数量,以及两个缓冲区的指针和大小。当此调用返回时,会使用 AttachHookArray 激活挂钩以拦截 Windows Live Messenger 的调用。

现在,将当前文件切换到 toolbar.cpp。代码的前几行并没有什么特别之处:

UINT uLastRcImg = RESOURCE_ID_BASE;
UINT uImgId;
DWORD dwBytePos = (ptbb->uPosition == TB_INSERT_FIRST ? 
                   TB_INSERT_FIRST_BYTE : ptbb->uPosition);

uLastRcImg 会在每次添加使用外部 DLL 资源的按钮时递增,并且它是一个足够大的值,不太可能与现有的位图资源 ID 冲突。uImgId 根据是否使用外部位图存储最终的资源 ID,而 dwBytePos 是要插入 XML 按钮定义的绝对字节对齐位置。

主要工作在 for 循环内完成。对于每次迭代:

  • 定义一个 XML 用户界面元素资源并将其复制到 tmpRes 字符数组中。
  • 生成一个匹配的样式定义,其中包含适当的字段,以便按钮能够执行其默认操作。这与支持 Microsoft Active Accessibility 的按钮有关,我们稍后会解释。当然,按钮 ID 和工具提示文本也会写入 tmpStyle 数组。
  • 在样式定义中,rcimg 属性被设置为指向一个内部 WLM 资源,或者对于外部 DLL 提供的资源,指向自定义的 uLastRcImg ID。
  • 如果位图图像来自外部 DLL(fExtBitmaptrue),则调用 Windows API 的资源管理函数以获取资源块和地址所需的句柄。请记住,对这些函数的挂钩是在此函数体退出时附加的,所以我们在这里可以安全地调用它们。成功获取此信息后,调用 RMC 成员函数 RegisterNewResource,参数为对应于 WLM 位图的资源类型 4000、为此新资源生成的 ID (uLastRcImg),以及已填充的 RESOURCEINFO 结构。这用于注册新的工具栏图像,而不是 XML UI 元素或样式资源。
  • 按钮点击事件使用按钮 ID 和函数指针在 CActionDispatcher 类实例上注册。关于这个类没有太多可谈的;它使用一个 std::map 来跟踪按钮操作,因此当需要触发所需操作时,会调用 ExecuteAction 成员函数,并通过 void 指针为 PFNBUTTONACTION 函数指针类型传递可选的用户数据。

当所有与新工具栏按钮相关的资源数据最终在 szStyleszXmlRes 中定义好后,就通过 RegisterResource 进行注册。使用 RegisterResource 而不是 RegisterNewResource 的原因很明确:我们正在修改现有的资源,包括样式表和元素定义 XML。相关的源代码行如下:

g_resMgr.RegisterResource(WLMRES_XMLSTYLE, WLMRES_CONTACT_LIST, 
   szStyle, strlen(szStyle), RR_INSERT, WLMRES_STYLE_INSERT_POS);
g_resMgr.RegisterResource(WLMRES_XMLUI, WLMRES_CONTACT_LIST, 
   szXmlRes, strlen(szXmlRes), RR_INSERT, dwBytePos);

对于我们项目的当前实现,两个插入位置都是根据 WLM 8.5 中的资源进行硬编码的。这是一个限制,可以通过实现至少一个最小的 WLM 资源数据 XML 解析引擎,或者使用 WLM_VERSION_MAJORWLM_VERSION_MINOR 宏进行条件编译来克服。只需注意引用有效的位置,以免搞乱 XML 结构。在 WLM 8.5 的情况下,我们通过转储所需的资源数据并使用十六进制查看器(如下所示的 FAR)来确定位置。

红框圈出的是位置(0x1524),即在资源 4005:923 中,紧跟在起始 MToolbar 标签之后,按钮定义开始的地方。

正如我们已经知道的,资源 4005:923 包含相关的样式定义;可以安全地在位置 23 (0x17) 处进行插入,即在头部之后:

<style resid=mainwndss>

在添加了两个工具栏按钮并注册了它们的资源后,RMC 注册资源表的状态应该类似于以下内容(未显示结构成员):

REGISTERED_RESOURCE_TABLE
类型和名称 ID RESOURCE_MOD_DESCRIPTOR
0x00000FA00000F618 RESOURCE_MOD_DESCRIPTOR 成员
0x00000FA00000F619 ...
0x00000FA40000039B ...
0x00000FA50000039B ...

这四个 64 位 ULONGLONG 值对应于两个自定义位图(4000:63000 和 4000:63001)、按钮的 XML UI 定义(4004:923)以及匹配的样式(4005:923)。

资源管理执行流程

在注册资源表填充完毕后,程序退出 AddToolbarButtons 的作用域,并为 FindResourceLoadResourceLockResourceSizeofResource 附加挂钩。现在,让我们看看如何引导 WLM 使用正确的资源,使我们的工具栏成功工作。请记住,此处的解释仅限于加载一个工具栏按钮;有关详细信息,请参阅 CResourceManager 类相关文件和定义了 API 挂钩处理程序的 reshook.cpp

  • Live Messenger 解析定义了 UI 元素的 XML 资源;这位于资源 4004:923。每个与注册资源表中的条目不匹配的资源 ID 都会被加载,而不会执行任何额外操作。
  • msncore.dll 中的 DirectUI 解析器到达我们插入的按钮标签时,它会在 4005:923 的样式数据中查找我们按钮对应的样式定义。它会遇到引用 ID 为 63000 和 63001 的位图资源的 rcimg 关键字,然后调用 FindResource 来获取资源 4000:63000 和 4000:63001 的 HRSRC 句柄,而这些资源在 Windows Live Messenger 的 msgsres.dll 资源 DLL 中当然是不存在的。在这种情况下,我们的 FindResource 挂钩处理程序知道它正在请求一个新资源,因此它会强制 API 返回指向我们提供的外部位图的 HRSRC,并将其插入到资源句柄表中。
  • Live Messenger 继续其将资源加载到内存中的一系列调用。当执行 LoadResource 时,其挂钩处理程序遵循我们上面解释的相同逻辑:如果正在加载一个新资源,它会返回位于 DLL 中的位图的 HGLOBAL。最后,它将该位图的 HGLOBAL 句柄添加到我们的资源指针表中。
  • 对于 LockResource 的拦截,挂钩处理函数调用 CResourceManager::AllocResource,该函数分配必要的虚拟内存来存储新的位图(如果使用了 RR_INSERT 修饰符,还会将新资源数据插入到现有资源中)。然后 LockResource 返回从外部 DLL 复制的新资源的地址,或者修改后资源现在所在的地址。
  • SizeofResource 挂钩的情况不遵循上述逻辑。因为 SizeofResource 可以在成功调用 FindResource 后的任何时刻被调用,所以对于已注册的资源,我们强制资源“管道”始终遵循 FindResource->SizeofResource 的顺序。在任何情况下,SizeofResource 的挂钩处理程序都将始终返回一个已存储的值。当 SizeofResource 处理程序发现这是第一次调用时,它会返回默认的、未修改的资源大小,该大小位于注册资源表中的 RESOURCE_MOD_DESCRIPTOR 实例中。后续的调用可能会返回新的修改后的资源大小(例如,RR_COPY 修饰符每次都会返回相同的大小)。

这只是对资源管理方式的一个非常粗略的描述;我鼓励您仔细研究源代码,当然,也可以将您自己的修改或想法应用到这个方案中。为了结束这次的阐述,下面展示了一张添加按钮后的截图:

在下一节中,您将学习如何通过 Active Accessibility 处理工具栏按钮的点击事件。

获取联系人列表选择

这是我们的最后一部分论述,涉及如何通过联系人的电子邮件(实际上是 MSN 联系人的唯一标识符)获取当前选定的联系人信息。请注意,要完全理解下面的阐述,您需要了解 COM 的基本理论:查询接口、IUnknown 指针、类工厂以及类似的概念。请记住,由于 COM 本质上是一种二进制组件架构,而不是基于源代码的架构,您可以将本节作为自己想法的基础,并用其他支持 COM 的语言(如 Visual Basic 或 Delphi)来实现它们。

Microsoft Active Accessibility (MSAA) 简介

Active Accessibility 是一种基于 Microsoft COM 的技术,旨在开发支持底层操作系统组件所支持的辅助功能特性的软件,从而可以“自然地”将例如为视障用户设计的特性添加到 Windows 应用程序中。因此,MSAA 的关键目标是设计具有可访问用户界面的软件,或轻松扩展当前软件以支持这些可访问功能。

您可能会问 MSAA 与 Live Messenger 的联系人有什么关系。答案在于,Windows Live Messenger 的 Direct-UI 界面支持 Active Accessibility,即使自绘控件在窗口侦测工具中是隐藏的,因此我们将利用这一点来窥探和挂钩这些控件。

为了开始探讨 MSAA 主题及其与 Windows Live Messenger 在实践中的关系,我们将使用 Accessibility SDK 中的 Accessibility Explorer 工具来窥探我们感兴趣的区域——联系人列表。

上面的截图显示了一个联系人的可访问性属性。考虑到每个支持可访问性的子控件(比如我们正在检查的联系人)都可以通过一个 COM 对象来访问,因此使用适当的接口来获取联系人 ID 是完全可能的。但是,通过哪个接口呢?答案很简单:所有具有基于 MSAA 的用户界面的应用程序都实现了 IAccessible。例如,使用一个有效的指向 IAccessible 的指针,很容易通过以下方式获取控件的状态:

pIAcc->get_accState(CHILDID_SELF, &v);

常量 CHILDID_SELF 指示该方法返回控件本身的信息,而不是其任何子控件的信息,而 v 是一个 VARIANT 类型的变量,其 lVal 成员包含对象的状态(vt 变体成员是 VT_I4)。有关对象状态常量VARIANT 结构,请参阅 MSDN。

有了这个快速入门,但在进入示例代码之前,我们将讨论 Windows Live Messenger 公开的 COM 接口。

Live Messenger 类型库

众所周知,Windows Live Messenger 提供了许多 COM 接口,可以通过使用指向 WLM 可执行文件类型库的 #import 指令直接在 Visual Studio 中使用。这些接口非常有用,足以编写出相当不错的插件来用于这款最流行的即时通讯应用;但有一个警告:使用这些接口会导致 Windows Messenger 的可执行文件 msmsgs.exe 启动,因为默认的类型库指向 Windows Messenger 的中继对象。由于 Windows Messenger 在后 XP 系统(如 Vista)上已不再可用,因此无法从默认类型库创建对象。这完全是不可取的。

解决方案是使用由 Fanatic Live 论坛上一位昵称为 'TheSteve' 的知名用户制作的自定义类型库;一个具有忽略 Windows Messenger 进程这一特性的自定义 TLB。根据那里的帖子,这位用户创建的这个出色的类型库在最新的 WLM 9.0 测试版上也能正常工作。更多信息,请参阅帖子 MessengerAPI without Windows Messenger。您将在 tlbs 目录中找到 MSNMessengerAPI.tlb 类型库。

现在,要获取指向 MSNMessenger 对象实例的指针,我们可以挂钩 CoCreateInstance,因为这是创建 COM 对象的常用函数。不幸的是,它无法捕获该对象的创建;然而,我们可以通过挂钩 CoRegisterClassObject 来成功拦截该对象实例的创建。相关的处理程序位于 comhook.cpp 源文件中:

void Handle_CoRegisterClassObject (NktHandlerParams * hp)
{    
    HRESULT            hr;
    WCHAR            wszBuf [256];
    CLSID            clsid = **(CLSID**)PARAMETER_INDEX(0);
    IUnknown*        pUnk = *(IUnknown**) PARAMETER_INDEX(1);
    IClassFactory*    pIcf = NULL;

    if (SUCCEEDED((HRESULT)WINAPI_RETVAL))
    {
        StringFromGUID(clsid, wszBuf);
        _OutputDebugString (L"S_OK, 
           Handling CoRegisterClassObject for CLSID %s", wszBuf);    

        // check CLSID ...
        if (clsid == MSNMessenger::CLSID_Messenger)
        {
            OutputDebugString (L"Handling CoRegisterClassObject for CLSID_Messenger\n");
            // get ptr to class factory
            hr = pUnk->QueryInterface(IID_IClassFactory, (void**)&pIcf);
            if (SUCCEEDED(hr))
            {
                hr = pIcf->CreateInstance (NULL, MSNMessenger::IID_IMSNMessenger, 
                    (void**) &g_wlmIfaces.pIMsn);

                _ASSERTE(hr == S_OK);                
            }            
            pUnk->Release();

            // attach hook to handle MSAA events
            g_wlmTopWindow.hwevh = SetWinEventHook(EVENT_OBJECT_FOCUS, 
                EVENT_OBJECT_STATECHANGE, GetModuleHandle(0), 
                &HandleWinEvent, GetCurrentProcessId(), NULL, 
                WINEVENT_INCONTEXT);
        }
    } 
}

逻辑很容易理解。对于每次成功的 CoRegisterClassObject 调用,我们检查它是否与所需的 MSNMessenger coclass 的 CLSID 相关。当我们捕获到它时,我们通过一个类工厂接口 (IID_IClassFactory) 创建该对象的一个实例,并将 IMSNMessenger 接口指针存储到 WLM_IFACES 结构的 pIMsn 成员中。

一旦我们正确地获取了 IMSNMessenger 接口指针,我们就着手挂钩 Active Accessibility 事件,目的是捕获联系人列表项的状态和焦点变化。我们使用 MSAA 提供的挂钩 API SetWinEventHook 而不是 Trappola,因为 Windows Live Messenger 的 DirectUI 控件不使用标准的 Win32 API 消息回调机制,因此我们之前审查的子类化方法不适用。但得益于 MSAA,所有控件都会“发出”可以通过这种 MSAA 挂钩功能拦截的消息。

捕获可访问性事件

当用户更改联系人的选择状态时,我们将获取联系人信息。最简单的方法是处理焦点更改事件 EVENT_OBJECT_FOCUS,并过滤掉除联系人列表用户以外的控件的焦点事件。对于我们添加的工具栏按钮的点击事件,我们处理 EVENT_OBJECT_STATECHANGE过滤按钮的“按下”状态变化。当按钮被点击时,我们调用 CActionDispatcher::ExecuteAction 方法,告诉动作分派器类,如果按钮 ID 已注册,则执行相应的函数;对于其余的默认按钮,Live Messenger 显然什么也不做。上面的代码是 MSAA 事件处理程序,它是在 SetWinEventHook API 函数中指定的。

void CALLBACK HandleWinEvent(HWINEVENTHOOK hook, DWORD event, HWND hwnd,
                                  LONG idObject, LONG idChild,
                                  DWORD dwEventThread, DWORD dwmsEventTime)
{
    IAccessible* pIAcc = 0;
    MSNMessenger::IMSNMessengerContact* pIMsnContact = 0;
    VARIANT varChild, varRole, varState;
    BSTR name = NULL;
    WCHAR wszID[MAXSTRL];

    VariantInit(&varChild);
    HRESULT hr = AccessibleObjectFromEvent (hwnd, idObject, idChild, &pIAcc, &varChild);

    if ((hr == S_OK) && (pIAcc != NULL)) 
    {
        if (pIAcc->get_accName(varChild, &name) != S_FALSE)
        {
            switch (event)
            {
            case EVENT_OBJECT_FOCUS:
                // process only if the contact info window menu option is enabled

                if (AccObjToContactID(name, wszID) && g_wlmTopWindow.fEnableInfoWnd)
                {    
                    BSTR bstrID = SysAllocString(wszID);                    
                    IDispatch* pIDisp;
                    hr = g_wlmIfaces.pIMsn->raw_GetContact(bstrID, 
                         g_wlmIfaces.pIMsn->MyServiceId, &pIDisp);
                    
                    if (SUCCEEDED(hr))
                    {
                        if (SUCCEEDED(pIDisp->QueryInterface(
                            MSNMessenger::IID_IMSNMessengerContact, 
                            (void**)&pIMsnContact)))
                        {
                            ShowWindow(g_wlmContactInfoWindow.hwnd, SW_SHOWNA);

                            // force window to repaint with new values
                            g_wlmContactInfoWindow.wszContactID = bstrID;
                            g_wlmContactInfoWindow.wszFriendlyName 
                              = pIMsnContact->FriendlyName;
                            g_wlmContactInfoWindow.msStatus = pIMsnContact->Status;
                            g_wlmContactInfoWindow.fBlocked = pIMsnContact->Blocked;
                            g_wlmContactInfoWindow.fCanPage = pIMsnContact->CanPage;
                            g_wlmContactInfoWindow.wszHomePhone 
                              = pIMsnContact->GetPhoneNumber(
                                MSNMessenger::MPHONE_TYPE_HOME);
                            g_wlmContactInfoWindow.wszWorkPhone
                              = pIMsnContact->GetPhoneNumber(
                                MSNMessenger::MPHONE_TYPE_WORK);
                            g_wlmContactInfoWindow.wszMobilePhone
                              = pIMsnContact->GetPhoneNumber(
                                MSNMessenger::MPHONE_TYPE_MOBILE);

                            SendMessage(g_wlmContactInfoWindow.hwnd, 
                                        WM_UPDATEBASECOLOR, 0, 0);
                            pIMsnContact->Release();
                        }
                        pIDisp->Release();
                    }
                    SysFreeString(bstrID);
                }    
                else // if it's focused to a non-contact element...
                {
                    // Hide contact info window
                    ShowWindow(g_wlmContactInfoWindow.hwnd, SW_HIDE);
                }
                break;

            case EVENT_OBJECT_STATECHANGE:
                // We handle this for toolbar buttons going to "pressed" state
                varRole.vt = VT_I4;
                pIAcc->get_accRole(varChild, &varRole);
                pIAcc->get_accState(varChild, &varState);
                if (varRole.lVal == ROLE_SYSTEM_BUTTONMENU)
                {            
                    if (varState.lVal == STATE_SYSTEM_PRESSED)
                    // Execute action related to this button name
                    g_actDisp.ExecuteAction(std::wstring(name), NULL);
                }
                break;
            }

            SysFreeString(name);
        }
        pIAcc->Release();
    }    
    VariantClear(&varChild);
}

我们不必深究细节,只需分析 MSAA 事件处理程序的逻辑:

  • 在调用 SetWinEventHook 时,要处理的事件范围被限制在 EVENT_OBJECT_FOCUSEVENT_OBJECT_STATECHANGE 之间(请参阅上一节中的 CoRegisterClassObject 挂钩处理程序)。因此,当此类事件发生时,我们首先使用 AccessibleObjectFromEvent 获取发出消息的可访问对象。如果对象名称有效,我们将其存储在 name 这个 BSTR 类型的变量中。
  • 对于焦点更改事件,我们从 IAccessible::get_accName 属性中验证并提取用户的登录名(ID)。之后,通过 IMSNMessenger 接口指针检索联系人信息,该指针又是从 IDispatch 指针获得的,因为 IMSNMessenger 是一个双重接口。这种接口允许客户端通过调度接口或标准的 vtable 机制访问其方法,旨在支持那些不支持通过 vtable 调用的语言(如 Visual Basic 6)进行 COM 客户端开发。
  • 对于状态更改事件,我们检查 Active Accessibility 的角色属性是否为 ROLE_SYSTEM_BUTTONMENU,并且状态是否变为按下(STATE_SYSTEM_PRESSED)。如果此条件为真,则通过 Action Dispatcher 类执行适当的操作。

这就完成了我们对 Active Accessibility 的特别解释,以及对本文技术的总体回顾。

结束语

我希望您喜欢这篇文章和代码。请将这篇论述视为 Windows Live Messenger 逆向工程和插件开发领域的基础文档,因为实现我们所做的工作还有许多技术和方法。正如我开头所说,这里应用的思想可以普遍地应用于任何 Windows 应用程序。

© . All rights reserved.