在早期的 Windows 操作系统中,在同一用户下运行的所有进程有着相同的安全等级,拥有相同的权限。此时进程可以自由地发送Windows 消息到其他进程的窗口。

从 Windows Vista 开始,对于某些 Windows 消息,这一方式再也行不通了,进程(或者其他的对象)开始拥有一个新的属性–特权等级(Privilege Level)。此时一个特权等级较低的进程不再可以向特权等级较高的进程发送消息,即便他们在相同的用户权限下运行也不行,这就是所谓的用户界面特权隔离(User Interface Privilege Isolation, 简称 UIPI)。

引入 UIPI 的目的是为了防止恶意程序发送消息给那些拥有较高权限的窗口,从而利用该进程的高权限达到某种目的等等。

一、UIPI 的运行机制

在 Windows 7 中,当 UAC(User Account Control)启用的时候,UIPI 的运行可以得到最明显的体现。在 UAC 中,当一个管理员用户登录系统后,操作系统会创建两个令牌对象(Token Object):一个是管理员令牌,拥有大多数特权(类似于 Windows Vista 之前的 System 中的用户),而另一个是经过过滤后的简化版本,只拥有普通用户的权限。

默认情况下,以普通用户权限启动的进程拥有普通特权等级【UIPI 的等级划分为低等级(low),普通(normal),高等级(high),系统(system)】。同样的,以管理员权限运行的进程(例如用户右键单击选择“以管理员身份运行”或者是通过添加“runas”参数调用 ShellExecute 运行的进程)拥有高(high)特权等级。

系统中会运行多种不同类型、不同特权等级的进程(当然,从技术上讲这两个进程都是在同一用户下)。我们可以使用Windows Sysinternals工具集中的Process Explorer查看各个进程的特权等级。

所以,当发现进程之间 Windows 消息通信发生问题时,不妨使用Process Explorer查看一下两个进程之间是否有合适的特权等级。

二、UIPI 所带来的限制

正如我们前文所说,等级的划分,是为了防止以下犯上。所以,有了用户界面特权隔离,一个运行在较低特权等级的应用程序的行为就受到了诸多限制,它不可以进行如下操作:

  • 验证由较高特权等级进程创建的窗口句柄
  • 通过调用 SendMessage 和 PostMessage 向由较高特权等级进程创建的窗口发送 Windows 消息
  • 使用线程钩子处理较高特权等级进程
  • 使用普通钩子(SetWindowsHookEx)监视较高特权等级进程
  • 向一个较高特权等级进程执行 DLL 注入

但是,一些特殊 Windows 消息是被容许的,因为这些消息对进程的安全性没有太大影响。这些 Windows 消息包括:

1
2
3
4
5
6
7
8
9
10
11
12
  0x000 - WM_NULL
  0x003 - WM_MOVE
  0x005 - WM_SIZE
  0x00D - WM_GETTEXT
  0x00E - WM_GETTEXTLENGTH
  0x033 - WM_GETHOTKEY
  0x07F - WM_GETICON
  0x305 - WM_RENDERFORMAT
  0x308 - WM_DRAWCLIPBOARD
  0x30D - WM_CHANGECBCHAIN
  0x31A - WM_THEMECHANGED
  0x313, 0x31B (WM_???)

三、修复 UIPI 问题

基于 Windows Vista 之前的操作系统行为所设计的应用程序,可能希望 Windows 消息能够在进程之间自由的传递以完成一些特殊的工作。当这些应用程序在 Windows 7/10/11 上运行时,因为 UIPI 机制的存在,这种消息传递被阻断了,应用程序就会遇到兼容性问题。

为了解决这个问题,Windows Vista 引入了新的 API 函数ChangeWindowMessageFilterChangeWindowMessageFilterEx。在特权等级较高的进程中利用这 2 个函数可以添加或者删除能够通过特权等级隔离的 Windows 消息。这就像拥有较高特权等级的进程设置了一个过滤器,被允许通过的 Windows 消息都将被添加到这个过滤器的白名单,只有在这个白名单上的消息才允许传递进来。

下面对添加/移除白名单功能进行了简单封装(UIPIMsgFilter函数),该函数可以针对特定的窗体添加、移除消息白名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
BOOL UIPIMsgFilter(HWND hWnd, UINT uMessageID, BOOL bAllow) {
OSVERSIONINFO VersionTmp;
VersionTmp.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&VersionTmp);
BOOL res = FALSE;

if (VersionTmp.dwMajorVersion >= 6) { // vista above.
BOOL(WINAPI * pfnChangeMessageFilterEx)(HWND, UINT, DWORD, PCHANGEFILTERSTRUCT);
BOOL(WINAPI * pfnChangeMessageFilter)(UINT, DWORD);

CHANGEFILTERSTRUCT filterStatus;
filterStatus.cbSize = sizeof(CHANGEFILTERSTRUCT);

HINSTANCE hlib = LoadLibrary(_T("user32.dll"));

if (hlib != NULL) {
(FARPROC &)pfnChangeMessageFilterEx = GetProcAddress(hlib, "ChangeWindowMessageFilterEx");

if (pfnChangeMessageFilterEx != NULL && hWnd != NULL) {
res = pfnChangeMessageFilterEx(hWnd, uMessageID, (bAllow ? MSGFLT_ADD : MSGFLT_REMOVE), &filterStatus);
}

// If failed, try again.
if (!res) {
(FARPROC &)pfnChangeMessageFilter = GetProcAddress(hlib, "ChangeWindowMessageFilter");

if (pfnChangeMessageFilter != NULL) {
res = pfnChangeMessageFilter(uMessageID, (bAllow ? MSGFLT_ADD : MSGFLT_REMOVE));
}
}
}

if (hlib != NULL) {
FreeLibrary(hlib);
}
}
else {
res = TRUE;
}

return res;
}

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。