获取系统托盘图标信息的尝试

目录

一、Win10 以下获取图标信息的基本原理

二、Win10 以下获取图标信息的注意点

三、Win 11 深入研究托盘图标信息发送方式(尝试)

3.1 逆向 Shell_NotifyIcon 函数

3.2 部分参数解释

3.3 实现调用方挂钩并解析数据结构

3.4 挂钩 CTray::v_WndProc 实现接收端的拦截

3.5 调试注入器

3.6 总结方案流程

四、获取托盘图标信息方案的改进方案

五、通知图标信息的迁移策略

六、参考文档


本文出处链接:htps://blog.csdn.net/qq_59075481/article/details/128594435。

系列文章:

编号文章标题AID
1

获取系统托盘图标信息的尝试

128594435
2获取 Windows 系统托盘图标信息的最新方案(一)136134195
3获取 Windows 系统托盘图标信息的最新方案(二)136199670
4获取 Windows 系统托盘图标信息的最新方案(三)136240462

前言:这里说一下这个系列主要部分的区别。首先这篇文章相当于是研究日志,所以写的很随意。阅读时,建议看后面几篇文章,尤其是最新方案(一)和(三)的部分。我只会在本篇中更新一些最新发现的内容,但可能暂时不作详细解释,具体的解释会在后面文章中整理出来(如果有时间的话)。其中,最新方案(一)扩展了本文截至 2024.02 月所更新内容的解释,最新方案(三)是对所采用方法的一个改进方法。警告:由于本人工作繁忙,本项目研究将长期缓慢更新甚至搁置!

一、Win10 以下获取图标信息的基本原理

使用 SendMessage 函数发送 TB_BUTTONCOUNT 消息到指定窗口,可以检索工具栏中当前按钮的计数。但这个值可能并不是最新的,个数仅仅是当前个数,而且并非是显示的个数,也非有效按钮窗口的个数。

使用 SendMessage 函数发送 TB_GETBUTTON 消息到指定窗口获取指针,然后向指定进程申请访问内存,获取 TBBUTTON 结构体中 dwData 字段数据,可以获得远线程的工具栏窗口中当前所有按钮的句柄、标题、进程路径信息。但该信息可能包含旧的内容。

任务栏的通知区域分为四个部分:用户提示的通知区域、系统升级的通知区域,溢出角通知区域、DUI弹出式通知区域(部分图标如:电源电量、音量等的注册记录在溢出通知区域窗口,但是并不是在溢出窗口内显示的,所以单独归类)。通知区域是外壳处理器Shell控件:ToolBarWindow32.

首先我们需要明白任务栏的 ToolBarWindow (工具栏视图)的一些特征:

1)除系统升级的通知区域以外的通知区域图标不会自动刷新,需要接收到鼠标移动消息、模拟点击消息才会逐个刷新。

2)如果一个窗口多次在通知区域注册通知图标,即使它是由同一个窗口发出的,也会被记录为独立的窗口,但他们具有相同的信息,在外壳程序内存中相应位置按序排列。


我们以前一般通过类似下面的代码实现获取系统通知区(俗称托盘)的图标数据,这个代码由于网上转载次数较多,就不考证最早出处了,反正原理就是获取外壳进程的数据,用未公开的结构,其实这是不稳定的,但被我们用了多年:

#include <iostream>
#include <windows.h>
#include <string>
#include <commctrl.h>
#include <atlstr.h>

using namespace std;

// 判断 x64 系统
BOOL Is64bitSystem()
{
	SYSTEM_INFO si;
	GetNativeSystemInfo(&si);
	if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ||
		si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64)
		return TRUE;
	else
		return FALSE;
}

// 获取托盘窗口句柄
HWND FindTrayWnd()
{
	HWND hWnd = NULL;

	hWnd = FindWindow(_T("Shell_TrayWnd"), NULL);
	hWnd = FindWindowEx(hWnd, NULL, _T("TrayNotifyWnd"), NULL);
	hWnd = FindWindowEx(hWnd, NULL, _T("SysPager"), NULL);
	hWnd = FindWindowEx(hWnd, NULL, _T("ToolbarWindow32"), NULL);

	return hWnd;
}

// 获取折叠托盘窗口句柄
HWND FindNotifyIconOverflowWindow()
{
	HWND hWnd = NULL;

	hWnd = FindWindow(_T("NotifyIconOverflowWindow"), NULL);
	hWnd = FindWindowEx(hWnd, NULL, _T("ToolbarWindow32"), NULL);

	return hWnd;
}

// 遍历窗口
BOOL EnumNotifyWindow(HWND hWnd)
{
	// 获取托盘进程ID
	DWORD dwProcessId = 0;
	GetWindowThreadProcessId(hWnd, &dwProcessId);
	if (dwProcessId == 0) {
		cout << "GetWindowThreadProcessId failed:" << GetLastError() << endl;
		return FALSE;
	}

	// 获取托盘进程句柄
	HANDLE hProcess = OpenProcess(
		PROCESS_VM_OPERATION |	// 需要在进程的地址空间上执行操作
		PROCESS_VM_READ |	// 需要使用 ReadProcessMemory 读取进程中的内存
		PROCESS_VM_WRITE,	// 需要在使用 WriteProcessMemory 的进程中写入内存
		FALSE,					// 子进程不继承句柄
		dwProcessId				// 目标进程 PID
	);
	if (hProcess == NULL) {
		cout << "OpenProcess failed:" << GetLastError() << endl;
		return FALSE;
	}

	// 在进程虚拟空间中分配内存,用来接收 TBBUTTON 结构体指针
	LPVOID p_tbbutton = VirtualAllocEx(
		hProcess,					// 目标进程句柄
		0,							// 内存起始地址(默认)
		4096,						// 内存大小
		MEM_COMMIT,					// 内存类型(提交)
		PAGE_EXECUTE_READWRITE		// 内存保护属性(可读可写可执行)
	);
	if (p_tbbutton == NULL) {
		cout << "VirtualAllocEx failed:" << GetLastError() << endl;
		return FALSE;
	}

	// 初始化
	DWORD dw_addr_dwData = 0;
	BYTE buff[1024] = { 0 };
	wstring ws_filePath = L"";
	wstring ws_tile = L"";
	HWND h_mainWnd = NULL;
	int i_data_offset = 12;
	int i_str_offset = 18;

	// 判断 x64
	if (Is64bitSystem()) {
		i_data_offset += 4;
		i_str_offset += 6;
	}

	// 获取托盘图标个数
	int i_buttons = 0;
	i_buttons = SendMessage(hWnd, TB_BUTTONCOUNT, 0, 0);
	if (i_buttons == 0) {
		cout << "TB_BUTTONCOUNT message failed:" << GetLastError() << endl;
		return FALSE;
	}



	// 遍历托盘
	for (int i = 0; i < i_buttons; i++) {
		// 获取 TBBUTTON 结构体指针
		if (!SendMessage(hWnd, TB_GETBUTTON, i, (LPARAM)p_tbbutton)) {
			cout << "TB_GETBUTTON message failed:" << GetLastError() << endl;
			return FALSE;
		}

		// 读 TBBUTTON.dwData(附加信息)
		if (!ReadProcessMemory(hProcess, (LPVOID)((DWORD)p_tbbutton + i_data_offset), &dw_addr_dwData, 4, 0)) {
			cout << "ReadProcessMemory failed:" << GetLastError() << endl;
			return FALSE;
		}

		// 读文本
		if (dw_addr_dwData) {
			if (!ReadProcessMemory(hProcess, (LPCVOID)dw_addr_dwData, buff, 1024, 0)) {
				cout << "ReadProcessMemory failed:" << GetLastError() << endl;
				return FALSE;
			}

			h_mainWnd = (HWND)(*((DWORD*)buff));
			ws_filePath = (WCHAR*)buff + i_str_offset;
			ws_tile = (WCHAR*)buff + i_str_offset + MAX_PATH;
			TCHAR szBuff[256];
			GetClassNameW(h_mainWnd, szBuff, sizeof(szBuff) / sizeof(TCHAR));//函数调用

			cout << "MainWindowHandleVale = " << hex << h_mainWnd << endl;
			wcout << "ExecuteFileBinPath = " << ws_filePath << endl;
			wcout << "WindowTitle = " << ws_tile << endl;
			printf("WindowClassName = %ws\n", szBuff);//控制台的输出类名
			printf("Press Enter to continue.");
			cin.get();
		}

		// 清理
		dw_addr_dwData = 0;
		h_mainWnd = NULL;
		ws_filePath = L"";
		ws_tile = L"";

		cout << endl;
	}
	if (VirtualFreeEx(hProcess, p_tbbutton, 0, MEM_RELEASE) == 0) {
		cout << "VirtualFreeEx failed:" << GetLastError() << endl;
		return FALSE;
	}
	if (CloseHandle(hProcess) == 0) {
		cout << "CloseHandle failed:" << GetLastError() << endl;
		return FALSE;
	}

	return TRUE;
}

int main()
{
	// 解决控制台中文
	setlocale(LC_ALL, "chs");

	// 获取托盘句柄
	HWND h_tray = FindTrayWnd();
	HWND h_tray_fold = FindNotifyIconOverflowWindow();

	// 遍历托盘窗口
	if (EnumNotifyWindow(h_tray) == FALSE || EnumNotifyWindow(h_tray_fold) == FALSE) {
		cout << "EnumNotifyWindow false." << endl;
	}
	printf("Press any Key to CLOSE Console.");
	cin.get();
	return 0;
}

现在,这个似乎在最新的 Win 11 上不起作用,想了解为什么的,请转到本文的第三部分。

二、Win10 以下获取图标信息的注意点

当开发者尝试使用如下代码获取任务栏图标个数时可能得不到准确的个数:

LRESULT ButtonCount = SendMessage(hWnd, TB_BUTTONCOUNT, 0, 0);// 获取hWnd窗口的图标个数

警告:以下谈到的案例并不局限于任务管理器,其他第三方软件都如此,这和资源管理器外壳Shell的处理逻辑有关系。

误区1:忽略系统升级的通知区域,尽管这个区域经常没有显示,但它位于溢出角按钮和用户通知区之间,当有程序首次创建通知区图标的时候,提供短暂突出显示的时间,这些图标临时显示在该区域,不在用户区和溢出区,当超时完成时(最长60秒)这些图标被放入溢出区(前提是没有被标记为始终显示)。在超时完成前通知统计的用户区和溢出区图标数据中跳过了这些图标。

误区2:忽略重复注册的图标窗口,尽管这在我们看来是相当荒诞的,但它确实发生过,也应该引起相关开发者的注意。

 这张图片展示了当任务管理器打开时(等待超时完成后记录的结果),在使用TB_BUTTONCOUNT获取个数时将多次记录到任务管理器窗口,显示为他创建了13个图标按钮,随后通过在外壳进程内存中读取TBBUTTON结构中的dwData数据,得到窗口的标题、句柄、以及目录路径(类名、矩形等其他信息是根据窗口句柄通过相关API遍历枚举的,不是直接获得的结果),不难发现同样的句柄有多条记录,然而我们不需要多余的信息。这是TB类消息得到错误结果的重要原因。

误区3:(对于误区2而言)任务管理器等刚刚注册通知图标的窗口,图标会显示在系统升级通知区,此时记录的个数是真确的(唯一的)。因为他只要接收消息了,就会被移除到溢出区。否则他只允许静默一段时间。

误区4:线程异常退出,而没有注销相应图标。这时候我们把它称作僵尸图标,空有躯壳而没有灵魂。但是这段内存区域中相关信息不会被立即清除。导致获得的计数以及句柄都是无效的。

通过检查窗口的矩形,API调用返回“Error:1400”无效的窗口句柄即可排除该错误带来的影响。

Notice:请观察下图数据

再看下图数据:

 以上结果发生在一个已经注册通知图标的程序意外终止时(没有来得及注销图标),外壳程序内存中不会及时删除这些内容,这些内容可能位于错误的内存位置,访问这些位置会带来意外的结果。即使用户已经刷新了视图区域,这些内容依然不会被清除,直到重启资源管理器。

并且对无效的句柄使用GetClassName()返回值为0,但是不会向遍历时候的字符串指针更新数据,所以需要注意变量的作用域或者记得及时检查错误原因,比如此时的LastError为1400,以及最后要记得将内容重新初始化为{0}。一面这些垃圾数据影响用户体验。(显然图中没有考虑到这些问题)

三、Win 11 深入研究托盘图标信息发送方式(尝试)

为了解决失效问题。最近我进行了一些积极的研究。

3.1 逆向 Shell_NotifyIcon 函数

也就是在突然间,我就想到在任务栏创建图标一般通过 Shell_NotifyIcon 函数。然后,就通过 IDA 逆向分析了一下这个函数。

我发现, Shell_NotifyIcon 函数并没有实现创建窗口的过程。它将 NOTIFYICONDATAW 结构封装在 WM_COPYDATA 中,并通过 SendMessageTimeout 发送给 explorer.exe 的 Shell_TrayWnd 窗口,真正的处理应该在 Shell_TrayWnd 的消息线程中。

首先,该函数查找了 Shell_TrayWnd 窗口的句柄,并且如果该窗口不存在(比如资源管理器未启动),则立即返回失败信息:

随后,对参数进行了一些基本的校验:

这里就是拷贝结构体数据到新的内存上(这里 IDA 的伪代码解析有问题):

获取 explorer.exe 进程句柄,获取进程二进制文件路径(QueryFullProcessImageNameW):

通过 Shell32 函数、COM 接口等获取调用方进程的完整路径:

修改调用方进程的消息过滤器设置,因为在 Vista ++,如果调用方进程指定的图标具有消息回调,但调用方进程的完整性级别高于资源管理器,则会导致 UIPI 阻止资源管理器将消息返回调用方,这会造成问题,所以,需要在过滤器中允许这类消息:

下面就是最关键的函数了, SendMessageTimeout 将 COPYDATASTRUCT 结构发送给 explorer.exe 的 Shell_TrayWnd 窗口,这里包含了要处理的结构体以及调用函数的类型:

3.2 部分参数解释

粘贴一段 Geoff Chappell 关于这类函数实现原理的原文(但是据大佬说,从 NT6.0 开始一些细节就有变化了,我还在继续分析):

The calling process is typically not the process that implements the taskbar window. The standard method of passing data to a window function in another process is with the WM_COPYDATA message. This provides for passing a DWORD in the dwData member of the COPYDATASTRUCT and/or an arbitrary amount in a buffer described by the lpData and cbData members. SHELL32 passes both. The DWORD serves to identify which function the data in the buffer is being passed for:

调用方进程通常不是实现任务栏通知窗口的进程。在另一个进程中传递数据到一个窗口过程的标准方法是使用 WM_COPYDATA 消息。WM_COPYDATA 消息提供了在 COPYDATASTRUCT 结构的 dwData 成员中传递一个 DWORD 值或者在 lpData 和 cbData 成员描述的缓冲区中传递任意数量的 DWORD 值的方法。SHELL32 同时支持这两种传递模式。下表列出的 DWORD 值用于标识缓冲区中传递的数据用于调用哪个函数:

0SHAppBarMessage
1Shell_NotifyIcon
2SHEnableServiceObject or SHLoadInProc

The buffer layout for SHAppBarMessage is:

传递 SHAppBarMessage 函数调用的参数的缓冲区布局为:

OffsetSizeRemarks
0x000x28 bytesenhanced APPBARDATA structure, with cbSize set to 0x28 and lParam sign-extended to QWORD
0x28dworddwMessage argument
0x2Cdwordhandle to a copy of the enhanced APPBARDATA in shared memory, else NULL
0x30dwordprocess ID of caller
0x340x04 bytesunused, presumed to be padding from QWORD alignment

(翻译)

偏移大小备注
0x000x28 字节APPBARDATA 结构的扩展,cbSize 设置为 0x28, lParam 符号扩展为QWORD 类型
0x28DWORD对应于 dwMessage 参数
0x2CDWORD位于共享内存中 APPBARDATA 扩展结构的副本的句柄,否则为 NULL
0x30DWORD调用方进程 ID
0x340x04 字节该字段尚未使用,假定是为 QWORD 填充的对齐字节

The point to the shared memory and process ID is that for some values of dwMessage, the SHAppBarMessage function is expected to return information in members of the APPBARDATA structure. However, the WM_COPYDATA mechanism copies data only from the source to the taskbar window, without providing a means to write back. The solution found by SHELL32 is that in the cases where it is needed, the enhanced APPBARDATA that is anyway passed in the buffer is also copied to shared memory and the means for accessing this shared memory is also passed in the buffer.

共享内存和进程 ID 的关键在于,对于 dwMessage 的某些值,SHAppBarMessage 函数需要返回 APPBARDATA 结构成员中的信息。然而,WM_COPYDATA 消息的机制只将数据从源复制到任务栏窗口,而不提供回写的方法。SHELL32 给出的解决方案是:在需要的情况下,无论如何在缓冲区中传递的 APPBARDATA 扩展结构也被复制到共享内存中,并且访问该共享内存的方法也在缓冲区中传递。

For Shell_NotifyIcon, the data for the buffer is laid out as follows. In the symbol file for EXPLORER.EXE, Microsoft publishes a name for this otherwise undocumented structure: TRAYNOTIFYDATAW.

对于 Shell_NotifyIcon 函数,其缓冲区的数据结构如下表所示。在 explorer.exe 的符号文件中,微软为这个未记录的结构发布了一个名称:TRAYNOTIFYDATAW。

OffsetSizeRemarks
0x00dwordsignature: 0x34753423
0x04dworddwMessage argument
0x080x03B8 bytesNOTIFYICONDATAW structure in current layout, constructed on stack from input at lpdata

(翻译)

偏移大小备注
0x00DWORD该值写为:0x34753423,explorer 通过该签名确定调用方不是普通的 WM_COPYDATA
0x04DWORDdwMessage 参数
0x08

0x03B8

字节

表示当前缓冲区布局中的 NOTIFYICONDATAW 结构,该结构在 lpData 的指向堆栈上构造

The SHEnableServiceObject and SHLoadInProc functions reduce to one operation differentiated by flags. The buffer layout is:

SHEnableServiceObjec t和 SHLoadInProc 函数缩减为一个由标识符区分的操作。缓冲区布局如下:

OffsetSizeRemarks
0x000x10 bytesCLSID from rclsid argument
0x10dword0x01 for SHLoadInProc
0x02 for SHEnableServiceObject if fEnable argument is FALSE
0x03 for SHEnableServiceObject if fEnable argument is non-zero

(翻译)

偏移大小备注
0x000x10 字节类型为 CLSID。该值由 rclsid 参数指定。
0x10DWORD0x01 为 SHLoadInProc
0x02 为 SHEnableServiceObject 如果 fEnable 参数为 FALSE
0x03 为 SHEnableServiceObject 如果 fEnable 参数为 非零值

最后清除对象占用的内存,函数返回:

综上,shell32api 用 WM_COPYDATA 拷贝 TRAYNOTIFYDATAW (NOTIFYICONDATAW 的超集)扩展结构,来实现跨进程传递数据。Shell_NotifyIcon 本身不会创建托盘(应该叫通知区)图标。它只是一个发送者,真正的实现在 explorer 的 Shell 里面。

3.3 实现调用方挂钩并解析数据结构

根据 Geoff Chappell 关于这类函数实现原理的研究。我们知道 SendMessageTimeoutW 在被 Shell_NotifyIcon 函数调用时候,参数分别为:

参数 1:Shell_TrayWnd 窗口句柄

参数 2:WM_COPYDATA (0x4A) 消息

参数 3:调用方的窗口句柄

参数 4:COPYDATASTRUCT 结构体

参数 5:指定 SendMessageTimeoutW 发送消息方式的标识符

参数 6:超时时间

参数 7:返回状态

下面我们采用 WinDbg 调试分析该函数,首先设置 USER32!SendMessageTimeoutW 断点,让测试程序(调用 Shell_NotifyIcon 函数)跑起来并触发断点。

WinDbg 调试结果:

Breakpoint 0 hit
USER32!SendMessageTimeoutW:
00007ffc`be1f53a0 4053            push    rbx
0:000> r
rax=000000f4d95ee338 rbx=00000000000000cf rcx=00000000000100fc
rdx=000000000000004a rsi=0000000000000000 rdi=00007ff616d45630
rip=00007ffcbe1f53a0 rsp=000000f4d95ee2b8 rbp=000000f4d95ee3c0
 r8=0000000000050e7c  r9=000000f4d95ee308 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=00000000000100fc
r14=0000000000000005 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
USER32!SendMessageTimeoutW:
00007ffc`be1f53a0 4053            push    rbx

我们知道,x64 的前 4 个参数是在寄存器上的,分别对应:

rcx = 00000000000100fc  == > HWND hTrayWnd

rdx = 000000000000004a  == > UINT Msg

r8   = 0000000000050e7c  == > HWND hMainWnd

r9   = 000000f4d95ee308  == > PCOPYDATASTRUCT

r8 表明了调用方窗口,r9 寄存器就是我们要找的数据,是指向 COPYDATASTRUCT 结构体的指针。

该结构体的声明如下:

typedef struct tagCOPYDATASTRUCT {
    ULONG_PTR dwData;
    DWORD cbData;
    _Field_size_bytes_(cbData) PVOID lpData;
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

其中,dwData 就是要执行函数的编号,也就是 Geoff Chappell 给的那个表:

0SHAppBarMessage
1Shell_NotifyIcon
2SHEnableServiceObject or SHLoadInProc

至于这个表有没有新增,我暂时还没验证。

不过由于我们测试用的是 Shell_NotifyIcon,则结果应该是 1,WinDbg 看一下,发现对的上:

0:000> dps 000000f4d95ee308
000000f4`d95ee308  00000000`00000001
000000f4`d95ee310  00000000`000005cc
000000f4`d95ee318  000000f4`d95ee360
000000f4`d95ee320  00000000`00000020
000000f4`d95ee328  00007ffc`00000000
000000f4`d95ee330  000001fa`7544f160
000000f4`d95ee338  00000000`00000000
000000f4`d95ee340  00000000`40000163
000000f4`d95ee348  00000000`40000163
000000f4`d95ee350  000001fa`75430000
000000f4`d95ee358  00000000`00000000
000000f4`d95ee360  00000000`34753423
000000f4`d95ee368  00050e7c`000003bc
000000f4`d95ee370  00000007`0000245f
000000f4`d95ee378  0001002b`000007ed
000000f4`d95ee380  76d86258`8bd56d4b

那么 lpData 是什么呢,我们继续用 WinDbg 看:

0:000> dps 000000f4`d95ee360
000000f4`d95ee360  00000000`34753423
000000f4`d95ee368  00050e7c`000003bc
000000f4`d95ee370  00000007`0000245f
000000f4`d95ee378  0001002b`000007ed
000000f4`d95ee380  76d86258`8bd56d4b
000000f4`d95ee388  5e8f7a0b`75285e94
000000f4`d95ee390  00000000`00000000
000000f4`d95ee398  00000000`00000000
000000f4`d95ee3a0  00000000`00000000
000000f4`d95ee3a8  00000000`00000000
000000f4`d95ee3b0  00000000`00000000
000000f4`d95ee3b8  00000000`00000000
000000f4`d95ee3c0  00000000`00000000
000000f4`d95ee3c8  00000000`00000000
000000f4`d95ee3d0  00000000`00000000
000000f4`d95ee3d8  00000000`00000000

由于中途关闭了一次电脑,记录时候来重新写的,下图是前面几个成员的解释:

WinDbg 参数的分析

和 Geoff Chappell 说的类似,是 NOTIFYICONDATAW 结构,但有所变化的是,第一个成员前面多了一个Signature 并且等于 0x34753423,应该是为了验证来源。

此外,可能是因为结构问题,HICON hIcon 成员对应位置需要改成 DWORD uKnown,这个值在测试过程中是 01002b,不清楚他是什么,根据 IDA 反汇编的代码,ICON 是创建之后才单独去绘制的,所以可能结构体有所变化,其他位置目前没有发现变化,完整的结构体如下:

typedef struct _TRAY_ICON_DATAW {
    LONGLONG Signature;
    DWORD cbSize;
    DWORD hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    DWORD uIconID; // HICON hIcon; why?
#if (NTDDI_VERSION < NTDDI_WIN2K)
    WCHAR  szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
    WCHAR  szTip[128];
    DWORD dwState;
    DWORD dwStateMask;
    WCHAR  szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
    union {
        UINT  uTimeout;
        UINT  uVersion;  // used with NIM_SETVERSION, values 0, 3 and 4
    } DUMMYUNIONNAME;
#endif
    WCHAR  szInfoTitle[64];
    DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
    GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
    HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;

我们利用 Detours 挂钩 USER32!SendMessageTimeoutW 函数,再次测试运行,结果成功解析出结构体:

#include <iostream>
#include <windows.h>
#include <tchar.h>  
#include <shellapi.h>
#include <string.h>
#include <detours.h>

#pragma comment(lib, "detours.lib")

#define WM_IAWENTRAY  WM_USER + 0x5  // 系统托盘的自定义消息

// 定义原始函数指针类型
typedef BOOL(WINAPI* RealSendMessageTimeoutW)(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam,
    UINT fuFlags,
    UINT uTimeout,
    PDWORD_PTR lpdwResult
    );


typedef struct _TRAY_ICON_DATAW {
    LONGLONG Signature;
    DWORD cbSize;
    DWORD hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    DWORD uIconID; // HICON hIcon; why?
#if (NTDDI_VERSION < NTDDI_WIN2K)
    WCHAR  szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
    WCHAR  szTip[128];
    DWORD dwState;
    DWORD dwStateMask;
    WCHAR  szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
    union {
        UINT  uTimeout;
        UINT  uVersion;  // used with NIM_SETVERSION, values 0, 3 and 4
    } DUMMYUNIONNAME;
#endif
    WCHAR  szInfoTitle[64];
    DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
    GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
    HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;


// 保存原始函数指针
LPVOID Real_SendMessageTimeoutW = nullptr;
// 定义全局变量  
NOTIFYICONDATAW e = { 0 };

// 定义挂钩函数
BOOL WINAPI MySendMessageTimeoutW(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam,
    UINT fuFlags,
    UINT uTimeout,
    PDWORD_PTR lpdwResult
)
{
    // 显示调用参数
    std::wcout << "SendMessageTimeoutW called:" << std::endl;
    std::wcout << "hWnd: " << std::hex << hWnd << std::endl;
    std::wcout << "Msg: " << std::hex << Msg << std::endl;
    std::wcout << "wParam: " << std::hex << wParam << std::endl;
    std::wcout << "lParam: " << std::hex << lParam << std::endl;

    // 如果 lParam 指向 WM_COPYDATA 结构体
    if (Msg == WM_COPYDATA) {
        PCOPYDATASTRUCT lpCopyData = reinterpret_cast<PCOPYDATASTRUCT>(lParam);
        PTRAY_ICON_DATAW lpIconData = reinterpret_cast<PTRAY_ICON_DATAW>(lpCopyData->lpData); // x64 系统偏移量 0x58
        std::wcout << L"pData: " << std::hex << lpIconData << std::endl;
        std::wcout << L"hWnd: " << std::hex << lpIconData->hWnd << std::endl;
        std::wcout << L"szTip: " << std::hex << lpIconData->szTip << std::endl;
        std::wcout << L"szInfo: " << std::hex << lpIconData->szInfo << std::endl;
        std::wcout << L"szInfoTitle: " << std::hex << lpIconData->szInfoTitle << std::endl;
    }
    // 调用原始函数
    BOOL result = ((RealSendMessageTimeoutW)Real_SendMessageTimeoutW)(
        hWnd, Msg, wParam, lParam, fuFlags, uTimeout, lpdwResult);
    return result;
}

void SetConsoleCodePageToUTF8() {  // Utf-8 为了兼容中文
    _wsetlocale(LC_ALL, L".UTF8");
}

int main()
{
    SetConsoleCodePageToUTF8();
    // 挂钩 SendMessageTimeoutW
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    Real_SendMessageTimeoutW = &SendMessageTimeoutW;
    DetourAttach(&(PVOID&)Real_SendMessageTimeoutW, MySendMessageTimeoutW);
    DetourTransactionCommit();
    std::cout << "Hello World!\n";
	// 初始化NOTIFYICONDATA结构
    e.cbSize = (DWORD)sizeof(NOTIFYICONDATAW);
    e.hWnd = GetConsoleWindow();
    e.uID = 0x5;
    e.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
    e.uCallbackMessage = WM_IAWENTRAY;
    e.hIcon = LoadIconW(NULL, IDI_APPLICATION);
    wcscpy_s(e.szTip, L"VC系统托盘程序");
    wcscpy_s(e.szInfo, L"标题");
    wcscpy_s(e.szInfoTitle, L"内容");
    e.dwInfoFlags = NIIF_INFO;
    e.uTimeout = 5000;
    Shell_NotifyIconW(NIM_ADD, &e);  // 在托盘区添加图标
	getchar();
	Shell_NotifyIconW(NIM_DELETE, &e);  // 删除图标
    // 卸载挂钩
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&(PVOID&)Real_SendMessageTimeoutW, MySendMessageTimeoutW);
    DetourTransactionCommit();
	return 0;
}

测试截图:

测试结果截图 1

托盘图标提示信息:

测试结果截图 2

说明,确实可以考虑对 WM_COPYDATA 消息的处理,来监视托盘图标信息。同时感谢 Geoff Chappell 的研究。

3.4 挂钩 CTray::v_WndProc 实现接收端的拦截

为了能够进一步获取所有图标注册的信息,而不是去全局挂钩所有窗口进程,我们势必需要研究 explorer 进程的窗口消息处理过程,通过一番分析,我们最终找到了 CTray 类,并在其中找到了 v_WndProc 回调函数。

在 x64 系统下,该函数的声明(IDA 给出,暂未修正)如下:

__int64 __fastcall CTray::v_WndProc(CTray *this, HWND hWnd, __int64 a3, ITEMIDLIST *a4, LPITEMIDLIST pidl)

该函数关键部分中响应 WM_COPYDATA(0x4A) 消息:

CTray::v_WndProc 中响应 WM_COPYDATA 消息的部分

为此,我们临时编写了下面的 HOOK 测试代码(偏移量为硬编码):

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include "detours.h"
#include <tchar.h>
#include <cstdio>
#include <string>
#include <shtypes.h>
#pragma comment(lib, "detours.lib")

HINSTANCE g_hinstDLL = NULL;
LPVOID fpVWndProc = NULL;
HWND Shell_TrayWnd = NULL;
HWND StartButton = NULL;
HWND TrayNotifyWnd = NULL;
HWND TrayClockWClass = NULL;
HWND Progman = NULL;

#define MAX_EXPLORER 16
INT ExplorerWndCount = 0;
HWND ExplorerWnd[MAX_EXPLORER] = { NULL };

std::string make_hwnd_text(HWND hwnd)
{
#define CHECK_HWND(hwnd, var) if (hwnd == var) return #var;
    CHECK_HWND(hwnd, Shell_TrayWnd);
    CHECK_HWND(hwnd, StartButton);
    CHECK_HWND(hwnd, TrayNotifyWnd);
    CHECK_HWND(hwnd, TrayClockWClass);
    CHECK_HWND(hwnd, Progman);
    for (INT i = 0; i < ExplorerWndCount; ++i)
    {
        CHECK_HWND(hwnd, ExplorerWnd[i]);
    }

    char buf[32];
    wsprintfA(buf, "%p", hwnd);
    return buf;
#undef CHECK_HWND
}

#define HWND2TEXT(hwnd) make_hwnd_text(hwnd).c_str()


void log_printf(const char* fmt, ...)
{
    DWORD dwError = GetLastError();

    char buf[512];
    va_list va;
    va_start(va, fmt);
    wvsprintfA(buf, fmt, va);
    va_end(va);

    TCHAR szPath[MAX_PATH];
    GetModuleFileName(g_hinstDLL, szPath, MAX_PATH);
    TCHAR* pch = _tcsrchr(szPath, TEXT('\\'));
    if (pch == NULL)
        pch = _tcsrchr(szPath, TEXT('/'));
    lstrcpy(pch + 1, TEXT("SysNotifyLog.txt"));

    HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("SysNotifyHooker Mutex"));
    WaitForSingleObject(hMutex, 800);
    {
        using namespace std;
        FILE* fp = NULL;
        _tfopen_s(&fp, szPath, TEXT("a"));
        if (fp)
        {
            fprintf(fp, "PID:%08lX:TID:%08lX:TIC:%08lX> ",
                GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount());
            fputs(buf, fp);
            fflush(fp);
            fclose(fp);
        }
    }
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);

    SetLastError(dwError);
}

void show_window_info(const char* name, HWND hwnd)
{
    DWORD pid, tid;
    tid = GetWindowThreadProcessId(hwnd, &pid);
    log_printf("<%s> HWND: %p, PID: 0x%08lX, TID: 0x%08lX\n", name, hwnd, pid, tid);
}

BOOL CALLBACK EnumExplorerProc(HWND hwnd, LPARAM lParam)
{
    if (ExplorerWndCount >= MAX_EXPLORER)
        return FALSE;   // stop

    TCHAR szClass[128];
    GetClassName(hwnd, szClass, 128);
    if (lstrcmpi(szClass, TEXT("CabinetWClass")) == 0)
    {
        HWND CabinetWClass = hwnd;
        show_window_info("CabinetWClass", CabinetWClass);
        ExplorerWnd[ExplorerWndCount++] = CabinetWClass;
    }
    else if (lstrcmpi(szClass, TEXT("ExploreWClass")) == 0)
    {
        HWND ExploreWClass = hwnd;
        show_window_info("ExploreWClass", ExploreWClass);
        ExplorerWnd[ExplorerWndCount++] = ExploreWClass;
    }

    return TRUE;    // continue
}

void show_info()
{
    log_printf("### WMNotifyHooker 2024.02.14 by Lianyou516 ###\n");

    Shell_TrayWnd = FindWindow(TEXT("Shell_TrayWnd"), NULL);
    show_window_info("Shell_TrayWnd", Shell_TrayWnd);

    StartButton = FindWindowEx(Shell_TrayWnd, NULL, TEXT("BUTTON"), NULL);
    if (StartButton == NULL)
        StartButton = FindWindowEx(Shell_TrayWnd, NULL, TEXT("Start"), NULL);
    show_window_info("StartButton", StartButton);

    TrayNotifyWnd = FindWindowEx(Shell_TrayWnd, NULL, TEXT("TrayNotifyWnd"), NULL);
    show_window_info("TrayNotifyWnd", TrayNotifyWnd);

    TrayClockWClass = FindWindowEx(TrayNotifyWnd, NULL, TEXT("TrayClockWClass"), NULL);
    show_window_info("TrayClockWClass", TrayClockWClass);

    Progman = FindWindow(TEXT("Progman"), NULL);
    show_window_info("Progman", Progman);

    EnumWindows(EnumExplorerProc, 0);
}


typedef LRESULT (__fastcall* __v_WndProc)(void* This, HWND hWnd, UINT64 uMsg, ITEMIDLIST* a4, LPITEMIDLIST pidl);

LRESULT __fastcall v_WndProc(void* This, HWND hWnd, UINT64 uMsg, ITEMIDLIST* a4, LPITEMIDLIST pidl)
{
    LRESULT ret = 0;
    ret = ((__v_WndProc)fpVWndProc)(This, hWnd, uMsg, a4, pidl);

    if ((UINT)uMsg == WM_COPYDATA)
    {
        COPYDATASTRUCT* pCopyData = reinterpret_cast<COPYDATASTRUCT*>(pidl);
        log_printf("CTray::v_WndProc -- COPYDATA: (HWND[%s], lpData[%I64X], cbData[%u], dwData[%Id]);\n",
            HWND2TEXT(hWnd),
            reinterpret_cast<UINT64>(pCopyData->lpData), pCopyData->cbData, pCopyData->dwData);
        log_printf("CTray::v_WndProc: leave: ret = %Id;\n", ret);
    }
    return ret;

}


void StartHookingFunction()
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息  
    DetourUpdateThread(GetCurrentThread());
    // 测试时硬编码偏移量
    fpVWndProc = reinterpret_cast<LPVOID>(reinterpret_cast<UINT64>(GetModuleHandleW(L"explorer.exe")) + 0xB630u);

    //将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。

    DetourAttach(&fpVWndProc,
        v_WndProc);

    //结束事务
    DetourTransactionCommit();
}

void UnmappHookedFunction()
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息 
    DetourUpdateThread(GetCurrentThread());

    //将拦截的函数从原函数的地址上解除,这里可以解除多个函数。

    DetourDetach(&fpVWndProc,
        v_WndProc);

    //结束事务
    DetourTransactionCommit();
}


extern "C"
BOOL WINAPI
DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    DisableThreadLibraryCalls(hinstDLL);
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        g_hinstDLL = hinstDLL;
        log_printf("DLL_PROCESS_ATTACH: %p\n", hinstDLL);
        show_info();
        StartHookingFunction();
        break;
    case DLL_PROCESS_DETACH:
        log_printf("DLL_PROCESS_DETACH: %p\n", hinstDLL);
        UnmappHookedFunction();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

经过注入测试,得到日志如下:

日志文件截图

但是对 lpData 位置的解析我觉得有问题:

0x3D7F560 是一个固定的地址,explorer 将一些信息存放在这里。

有趣的是在寻找是否已经存在解决方案的时候,我意外找到了 ReactOS 团队于 2017 年早期测试 explorer.exe 挂钩的开源代码,虽然在最新的系统上已经不再适用,并且它主要解析explorer 作为消息发送者的情况,而不是接收者,但是他们的模板可能有所帮助。

Github 链接如下:SysNotifyHooker: API hook for Windows Explorer

2024/02/15 更新内容:

调试并修改了了一下 HOOK 代码:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include "detours.h"
#include <tchar.h>
#include <cstdio>
#include <string>
#include <shtypes.h>
#include <clocale>
#pragma comment(lib, "detours.lib")

HINSTANCE g_hinstDLL = NULL;
LPVOID fpVWndProc = NULL;

std::wstring make_hwnd_text(HWND hwnd)
{
    wchar_t buf[32];
    wsprintfW(buf, L"%p", hwnd);
    return buf;
}

#define HWND2TEXT(hwnd) make_hwnd_text(hwnd).c_str()


void log_printf(const wchar_t* fmt, ...)
{
    DWORD dwError = GetLastError();
    _wsetlocale(LC_ALL, L".UTF8");
    wchar_t buf[800];
    va_list va;
    va_start(va, fmt);
    vswprintf_s(buf, fmt, va);
    va_end(va);

    TCHAR szPath[MAX_PATH];
    GetModuleFileName(g_hinstDLL, szPath, MAX_PATH);
    TCHAR* pch = _tcsrchr(szPath, TEXT('\\'));
    if (pch == NULL)
        pch = _tcsrchr(szPath, TEXT('/'));
    lstrcpy(pch + 1, TEXT("SysNotifyLog.txt"));

    HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("SysNotifyHooker Mutex"));
    WaitForSingleObject(hMutex, 800);
    {
        using namespace std;
        FILE* fp = NULL;
        _tfopen_s(&fp, szPath, TEXT("a"));
        if (fp)
        {
            fprintf(fp, "PID:%08lX:TID:%08lX:TIC:%08lX> ",
                GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount());
            fputws(buf, fp);
            fflush(fp);
            fclose(fp);
        }
    }
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);

    SetLastError(dwError);
}


typedef LRESULT (__fastcall* __v_WndProc)(void* This, HWND hWnd, UINT uMsg, ITEMIDLIST* a4, LPITEMIDLIST pidl);

typedef struct _TRAY_ICON_DATAW {
    LONGLONG Signature;
    DWORD cbSize;
    DWORD hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    DWORD uIconID; // HICON hIcon; why?
#if (NTDDI_VERSION < NTDDI_WIN2K)
    WCHAR  szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
    WCHAR  szTip[128];
    DWORD dwState;
    DWORD dwStateMask;
    WCHAR  szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
    union {
        UINT  uTimeout;
        UINT  uVersion;  // used with NIM_SETVERSION, values 0, 3 and 4
    } DUMMYUNIONNAME;
#endif
    WCHAR  szInfoTitle[64];
    DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
    GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
    HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;

LRESULT __fastcall v_WndProc(void* This, HWND hWnd, UINT uMsg, ITEMIDLIST* a4, LPITEMIDLIST pidl)
{
    //log_printf(L"CTray::v_WndProc -- COPYDATA: (HWND[%ws], lpData[%I64X], cbData[%04X], dwData[%I64d]);\n",
                //HWND2TEXT(hWnd),
                //reinterpret_cast<UINT64>(&(pCopyData->lpData)), pCopyData->cbData, pCopyData->dwData);
    LRESULT ret = 0;
    ret = ((__v_WndProc)fpVWndProc)(This, hWnd, uMsg, a4, pidl);

    if ((UINT)uMsg == WM_COPYDATA)
    {
        COPYDATASTRUCT* pCopyData = reinterpret_cast<COPYDATASTRUCT*>(pidl);
        if (pCopyData->dwData == 1)
        {
            PTRAY_ICON_DATAW pTrayIcon = reinterpret_cast<PTRAY_ICON_DATAW>(pCopyData->lpData);

            if (pTrayIcon->Signature == 0x34753423)
            {
                //log_printf(L"CTray::v_WndProc -- COPYDATA: Signature = 0x34753423;\n");
                log_printf(L"CTray::v_WndProc -- COPYDATA: szTip[%ws], szInfoTitle[%ws], pTrayIcon->szInfo[%ws];\n",
                    pTrayIcon->szTip, pTrayIcon->szInfoTitle, pTrayIcon->szInfo);
            }
            log_printf(L"CTray::v_WndProc: leave: ret = %Id;\n", ret);
        }
    }
    return ret;
}


void StartHookingFunction()
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息  
    DetourUpdateThread(GetCurrentThread());
    // 测试时硬编码偏移量
    fpVWndProc = reinterpret_cast<LPVOID>(reinterpret_cast<UINT64>(GetModuleHandleW(L"explorer.exe")) + 0xB630u);

    //将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。

    DetourAttach(&fpVWndProc,
        v_WndProc);

    //结束事务
    DetourTransactionCommit();
}

void UnmappHookedFunction()
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息 
    DetourUpdateThread(GetCurrentThread());

    //将拦截的函数从原函数的地址上解除,这里可以解除多个函数。

    DetourDetach(&fpVWndProc,
        v_WndProc);

    //结束事务
    DetourTransactionCommit();
}


extern "C"
BOOL WINAPI
DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    DisableThreadLibraryCalls(hinstDLL);
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        g_hinstDLL = hinstDLL;
        log_printf(L"DLL_PROCESS_ATTACH: %p\n", hinstDLL);
        StartHookingFunction();
        //show_info();
        break;
    case DLL_PROCESS_DETACH:
        log_printf(L"DLL_PROCESS_DETACH: %p\n", hinstDLL);
        UnmappHookedFunction();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

// just for exporting a function
extern "C" __declspec(dllexport) void ImportFunc()
{
    // Do nothing
}

并在 winDbg64 下第一个 TLS 回调事件(此时已经过了保护阶段,explorer 准备加载不受保护的一些模块)时注入 explorer 进程,就可以拦截所有托盘图标信息:

测试结果截图 1
测试结果截图 2

可见,可以成功完成托盘图标信息的管理,目前还剩一个问题,就是如何通过编程实现在 explorer.exe 初始化时加载模块,目前暂停进程注入、APC 注入、窗口回调注入、导入表劫持等均失败(注入过早会失败,过晚了任务栏回调就结束了)。目前想到的一个方法就是 TLS 回调时注入和入口点劫持注入两种,但是问题在于入口点劫持可能比较晚了,所以,正在往 TLS 回调注入考虑。

想法:如果在编程中启用了TLS功能,PE 头文件中就会设置 TLS 表(IMAGE_NT_HEARDERS->IMAGE_OPTIONAL_HEADER->IMAGE_DATA_DIRECTORY[9])。那么我们可以使用 LocalSystem 服务,设置为开机自启动,然后服务进程在开机时关闭 explorer 进程,创建调试进程并在第一个 TLS 函数入口处写入软件断点指令,继续执行程序并在接收到断点异常事件后执行远程线程注入,在创建远程线程后立即释放进程,等待模块加载完成,则分离作为服务进程的调试器。

3.5 调试注入器

2024.02.16 更新

已经成功实现调试注入器,但对于其原理我仍然存在疑惑,为什么直接挂起注入会失败?所有的内容我将在新的文章中详细分析。

下面是初步实现的注入器截图:

调试注入器

代码暂时就不发了,还有些地方没弄明白,怕误人子弟。(新内容发布在《最新方案(一)》)

2024.02.20 更新

完善了注入模块,撰写在文章 《最新方案(一)》中,暂未修复创建挂起进程注入存在的问题。

完善了注册表途径的代码原理解释文档,撰写在《最新方案(二)》中。

2024.02.22 更新

完善了拦截 WM_COPYDATA 消息的处理方式,现在改用 Windows Hook ,内容初步撰写在《最新方案(三)》中。

2024.03.22 更新

找出了 Dll 注入代码中存在的缺陷,并确认了创建挂起进程可以完成注入但有概率造成崩溃。

2024.05.04 - 2024.05.13更新

添加对图标状态更改的判断原理、图标隐藏的实验性方案、 Windows Hook 方法的解释文本。

3.6 总结方案流程

我目前想到的不是直接用 COM,毕竟还没找到。我尝试在资源管理器初始化时注入例程,过滤 Shell_TrayWnd 窗口的 WM_COPYDATA 消息,因为任何通过 SHAppBarMessage, Shell_NotifyIcon, SHEnableServiceObject 和 SHLoadInProc 封装的内部实现都是向 Shell_TrayWnd 的消息线程发送 WM_COPYDATA 并带有特殊标志的缓冲区结构来共享系统通知栏的图标信息。

四、获取托盘图标信息方案的改进方案

(此部分具体见《最新方案(三)》,内容于2024.05.05 更新) 

应用程序通过 Shell_NotifyIcon 函数向系统通知区域注册“通知图标”。 Shell_NotifyIcon 函数仅仅是将 NOTIFYICONDATA 结构封装到 COPYDATASTRUCT (CDS) 中,通过 WM_COPYDATA 消息向 explorer.exe 共享缓冲区的信息。此缓冲区传递的结构包括了 NOTIFYICONDATA 结构包含的全部信息、要处理的图标状态更改(Shell_NotifyIcon 函数的 dwMessage 参数)以及调用方的一部分信息(调用方窗口句柄、进程完整路径等)。 NOTIFYICONDATA 结构是通知图标的相关信息。

当了解到这些情况后,我们还可以通过 Spy++ 工具知道通知区域处理此类 WM_COPYDATA 消息的窗口是“Shell_TrayWnd”。所有这些,在第一篇中已经通过逆向工程和相关文献被分析出来。

为了便于获取所传递结构的成员,我解析了 x64 系统下 UNICODE 版本的结构:

// x64 结构体的声明
typedef struct _TRAY_ICON_DATAW {
    DWORD Signature;
    DWORD dwMessage;   // dwMessage <-- Shell_NotifyIconW(DWORD dwMessage, ...)
    DWORD cbSize;
    DWORD hWnd;
    UINT uID;
    UINT uFlags;
    UINT uCallbackMessage;
    DWORD uIconID; // HICON hIcon; why it changes?
#if (NTDDI_VERSION < NTDDI_WIN2K)
    WCHAR  szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
    WCHAR  szTip[128];
    DWORD dwState;
    DWORD dwStateMask;
    WCHAR  szInfo[256];
#ifndef _SHELL_EXPORTS_INTERNALAPI_H_
    union {
        UINT  uTimeout;
        UINT  uVersion;  // used with NIM_SETVERSION, values 0, 3 and 4
    } DUMMYUNIONNAME;
#endif
    WCHAR  szInfoTitle[64];
    DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
    GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
    HICON hBalloonIcon;
#endif
} TRAY_ICON_DATAW, * PTRAY_ICON_DATAW;

是的,此内部结构存在 UNICODE 和 ASCII 两种版本。结构的第一个成员是一个哨兵值(签名),因为 WM_COPYDATA 可能传递多种不同结构,而为了便于接收消息的 explorer 程序能够正确处理不同的情况,内部采用了签名来验证消息的格式。

而传递通知图标结构体信息时的签名信息为 0x34753423,也就是说,接收方会检查该值是否是 0x34753423 来决定是否调用相应的处理。

而结构的第二个成员则是和 Shell_NotifyIcon 函数绑定的参数 dwMessage,该参数指定了需要对通知图标进行的操作。这里有几个不同的操作,包含创建图标、修改图标、删除图标等,只需要指定 Shell_NotifyIcon 的文档所规定的常量。

结构接下来的成员则基本和 NOTIFYICONDATA 结构相对应,只不过对 hWnd 和 hIcon 成员有所变化,他们转为了由 DWORD 值存储。

微软提供的 Win32 WindowsHook 可以实现在消息到达目标窗口前/后进行捕获,其中CallWndProcHook 就可以在消息被窗口处理前捕获,而 GetMessageHook 可以捕获到达窗口的消息,但是无法阻止消息处理。

通过创建一个 CallWndProcHook 或者 GetMessageHook 钩子例程,并同样利用  WM_COPYDATA 将发送至 Shell_TrayWnd 窗口的 WM_COPYDATA 转发至我们创建窗口。这将允许我们捕获通知区域图标的注册或者状态更改信息。

在这里,我们需要注意一点,使用 SendMessage(严禁使用,除非你知道你在做什么) 和 SendMessageTimeout 转发消息均可能因阻滞导致一系列问题,尤其是它们配合 CallWndProcHook 使用时,必须考虑清楚需要为 SMTO 设置的超时时间间隔(本文代码默认设置的是 10 秒)。如果为了安全起见,比如你只需要获取消息,而不需要改变消息的处理模式,则推荐使用 GetMessageHook 获取消息并配合 PostMessage 转发消息。

(2024.05.13)

我在一个论坛发现了一个有意思的讨论,他们的发现甚至更早,并且解决方案似乎更加完善,但我还没有验证这点.

Global System Tray Hook - AutoIt General Help and Support - AutoIt Forums

下面的代码均来自上文:

#ifndef __MINGW__
#pragma once
#endif /* __MINGW__ */

extern "C" __declspec(dllexport) bool WINAPI SetTrayHook( char* szServerName );
bool WINAPI UnsetTrayHook();
bool WINAPI IsNt();

#define GSSPY_DOWNLOAD  "GSSPY_DOWNLOAD"

DLL 的主要部分:

//
//
// geOTraySpy.cpp
//
// Main implementation file for geOTraySpy.dll
//
// This is based on Mike Lin's traysaver code.
//
// It builds a dll which contains code to establish a hook into
// the Shell_TrayWnd class, receiving all wndProc messages after they
// have been processed by Shell_TrayWnd itself.
//
// It then forwards any copydata messages to the geOShell TrayService.
//
//
//

#include <windows.h>
//#include "../geOLib/AggressiveOptimize.h"
#include <stdio.h>
//#include "tsdebug.h"
#include "geOTraySpy.h"

//
//
// Globals
//
//

#define PAD_SIZE    65

struct stTrayItem
{
    COPYDATASTRUCT  CopyData;

    struct stNicHolder
    {
        BYTE            bPad[8];
        NOTIFYICONDATA  IconData;
        CHAR            szPad[PAD_SIZE];    // The Tip can be up to 128 chars long
    }               NicHolder;
};

#define MAX_SAVEDTRAYDATA   25

#pragma data_seg( "shared" )

CHAR                g_cServerClass [100]    = "";
HWND                g_hTargetWin            = 0;
HWND                g_hTrayWin              = 0;
HHOOK               g_hHook                 = 0;
UINT                g_iNextFreePos          = 0;
struct stTrayItem   g_SavedTrayData[MAX_SAVEDTRAYDATA];

#pragma data_seg()

#pragma comment( linker, "/SECTION:shared,rws" )

const UINT WM_GSSPY_DOWNLOAD    = ::RegisterWindowMessage(GSSPY_DOWNLOAD);
const char CLASS_TrayProc[]     = "Shell_TrayWnd";

#ifndef NIF_INFO
#define NIF_INFO         0x00000010
#endif

typedef struct _MYNOTIFYICONDATA { 
    DWORD   cbSize; 
    HWND    hWnd; 
    UINT    uID; 
    UINT    uFlags; 
    UINT    uCallbackMessage; 
    HICON   hIcon; 
    TCHAR   szTip[128];
    DWORD   dwState;
    DWORD   dwStateMask;
    TCHAR   szInfo[256];
    union
    {
        UINT  uTimeout;
        UINT  uVersion;
    } MYDUMMYUNIONNAME;
    TCHAR   szInfoTitle[64];
    DWORD   dwInfoFlags;
} MYNOTIFYICONDATA, *PMYNOTIFYICONDATA;

//
//
// Functions
//
//

//
// IsNT
//
// Returns true if we are running under Windows NT.
//

bool WINAPI IsNT()
{
    static bool         bNT = false;
    static bool         bFound = false;
    OSVERSIONINFO       ovi;
    
    if( !bFound )
    {
        ovi.dwOSVersionInfoSize = sizeof( OSVERSIONINFO );

        GetVersionEx( &ovi );

        bNT = ovi.dwPlatformId == VER_PLATFORM_WIN32_NT;

        bFound = true;
    }

    return bNT;
}

//
// TrayWndProc
//
// New window proc for the system tray window
//

LRESULT CALLBACK TrayWndProc( int nCode, WPARAM wParam, LPARAM lParam )
{
    CWPRETSTRUCT *pInfo = reinterpret_cast< CWPRETSTRUCT * >( lParam );
    
    if( nCode >= 0 )
    {
        // Interested in copydata messages
        if ( pInfo->message == WM_COPYDATA )
        {
            
            // Make sure we have a valid handle on the Tray window
            if( !g_hTrayWin || !IsWindow( g_hTrayWin ) )
                g_hTrayWin = FindWindow( CLASS_TrayProc, NULL );

            // Make sure this message is from the Tray window
            if (IsWindow( g_hTrayWin) && pInfo->hwnd == g_hTrayWin )
            {
                //MessageBox(NULL, "WM_COPYDATA from tray...", "", MB_OK);
                COPYDATASTRUCT* pcds = reinterpret_cast< COPYDATASTRUCT* >( pInfo->lParam );

                if( pcds->dwData == 1 ) // if this data refers to a tray icon...
                {
                    //MessageBox(NULL, "is tray icon...", "", MB_OK);
                    // Maintain our own copy of the tray icons
                    NOTIFYICONDATA  *nicData = (NOTIFYICONDATA *)(((BYTE *)pcds->lpData) + 8);
                    INT             iTrayCmd = *(INT *)(((BYTE *)pcds->lpData) + 4);

                    if ((int)nicData->uID == -1)
                    {
                        MessageBox(NULL, "early return, calling next hook.", "", MB_OK);
                        return CallNextHookEx( g_hHook, nCode, wParam, lParam );
                    }

                    if (iTrayCmd == NIM_ADD || iTrayCmd == NIM_MODIFY)
                    {
                        //MessageBox(NULL, "itraycmd is add or mod", "", MB_OK);
                        bool    bFound = false;
                        UINT    i = 0;

                        for (i = 0; i < g_iNextFreePos && i < MAX_SAVEDTRAYDATA; i++)
                        {
                            if (g_SavedTrayData[i].NicHolder.IconData.uID == nicData->uID && 
                                g_SavedTrayData[i].NicHolder.IconData.hWnd == nicData->hWnd)
                            {


                                memcpy (&g_SavedTrayData[i].CopyData, pcds, sizeof (COPYDATASTRUCT));

                                if (iTrayCmd == NIM_ADD)
                                {
                                    memcpy (&g_SavedTrayData[i].NicHolder.IconData,
                                            nicData,
                                            (int) nicData->cbSize <= (sizeof (NOTIFYICONDATA) + PAD_SIZE) ? (int) nicData->cbSize : (sizeof (NOTIFYICONDATA) + PAD_SIZE));
                                }
                                else
                                {
                                    if ((nicData->uFlags | NIF_ICON && nicData->hIcon) || 
                                        (g_SavedTrayData[i].NicHolder.IconData.hIcon == NULL && nicData->hIcon))
                                    {
//                                      MessageBox(NULL, "Icon", "", MB_OK);
                                        g_SavedTrayData[i].NicHolder.IconData.hIcon = nicData->hIcon;
                                    }

                                    if ((nicData->uFlags | NIF_MESSAGE) ||
                                        (!g_SavedTrayData[i].NicHolder.IconData.uCallbackMessage && nicData->uCallbackMessage))
                                    {
//                                      MessageBox(NULL, "Msg", "", MB_OK);
                                        g_SavedTrayData[i].NicHolder.IconData.uCallbackMessage = nicData->uCallbackMessage;
                                    }

                                    if ((nicData->uFlags | NIF_TIP) ||
                                        (g_SavedTrayData[i].NicHolder.IconData.szTip[0] == '\0' && nicData->szTip[0] != '\0'))
                                    {
                                        memcpy (g_SavedTrayData[i].NicHolder.IconData.szTip, nicData->szTip, sizeof (nicData->szTip) + (int) nicData->cbSize - sizeof(NOTIFYICONDATA));
                                    }   

                                    g_SavedTrayData[i].NicHolder.IconData.uFlags = nicData->uFlags;
                                }

                                bFound = true;
                                break;
                            }
                        }

                        if (!bFound && (i < MAX_SAVEDTRAYDATA))
                        {
                            memcpy (&g_SavedTrayData[i].CopyData, pcds, sizeof (COPYDATASTRUCT));
                            memcpy (&g_SavedTrayData[i].NicHolder.IconData,
                                    nicData,
                                    (int) nicData->cbSize <= (sizeof (NOTIFYICONDATA) + PAD_SIZE) ? (int) nicData->cbSize : (sizeof (NOTIFYICONDATA) + PAD_SIZE));
                            g_iNextFreePos++;
                        }
                    }
                    else if (iTrayCmd == NIM_DELETE)
                    {
                        //MessageBox(NULL, "itraycmd is delete", "", MB_OK);
                        for (UINT i = 0; i < g_iNextFreePos && i < MAX_SAVEDTRAYDATA; i++)
                        {
                            if (g_SavedTrayData[i].NicHolder.IconData.uID == nicData->uID && 
                                g_SavedTrayData[i].NicHolder.IconData.hWnd == nicData->hWnd)
                            {
                                memmove(&g_SavedTrayData[i], &g_SavedTrayData[i + 1], sizeof(struct stTrayItem) * (g_iNextFreePos - (i + 1)));
                                g_iNextFreePos--;
                                break;
                            }
                        }
                    }

                    // Make sure we have a valid handle on the target window
                    if( !g_hTargetWin || !IsWindow( g_hTargetWin ) )
                        g_hTargetWin = FindWindow( g_cServerClass, NULL );
                    //MessageBox(NULL, "finding target window...", "", MB_OK);
                    // OK?
                    if (IsWindow( g_hTargetWin ))
                    {
                        //MessageBox(NULL, "sending WM_COPY", "", MB_OK);
                        // Forward the message
                        SendMessage( g_hTargetWin, 
                                     WM_COPYDATA, 
                                     reinterpret_cast< WPARAM >( g_hTrayWin ), 
                                     pInfo->lParam );
                        DWORD dw = GetLastError();
                        if (dw>0){
                        char szMyString[14] ={0};
                        sprintf(szMyString, "0x%08x",dw);
                        MessageBox(NULL, szMyString, TEXT("Message Sent, LastError:"), MB_OK) ;
                        }
                    }
                }
            }
        }
        else if ( pInfo->message == WM_GSSPY_DOWNLOAD )
        {
            if( !g_hTargetWin || !IsWindow( g_hTargetWin ) )
                g_hTargetWin = FindWindow( g_cServerClass, NULL );

            for (UINT i = 0; i < g_iNextFreePos && i < MAX_SAVEDTRAYDATA; i++)
            {
                g_SavedTrayData[i].CopyData.lpData = &g_SavedTrayData[i].NicHolder;

                SendMessage( g_hTargetWin, 
                             WM_COPYDATA, 
                             reinterpret_cast< WPARAM >( g_hTrayWin ), 
                             (long) &g_SavedTrayData[i] );
                
            }
        }
    }

    return CallNextHookEx( g_hHook, nCode, wParam, lParam );
}

BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
    return TRUE;
}

//
// SetTrayHook
//
// Sets our hook
//


__declspec(dllexport) bool WINAPI SetTrayHook (char* szServerName)
{
    DWORD dwThreadID = 0;
    //MessageBox(NULL, "Called Set", "", MB_OK);
    if( g_hHook )
        return false;
    //MessageBox(NULL, "n ghook", "", MB_OK);
    if (strlen(szServerName) > 99 || strlen(szServerName) == 0)
        return false;
    //MessageBox(NULL, "length ok", "", MB_OK);
    strcpy (g_cServerClass, szServerName);
    //MessageBox(NULL, "copied ok", "", MB_OK);
    if( IsNT() )
    {
        HWND hwndTray = FindWindow( CLASS_TrayProc, NULL );
        //MessageBox(NULL, "is nt", "", MB_OK);
        if( !hwndTray )
            return false;
        //MessageBox(NULL, "found class", "", MB_OK);
        if( hwndTray )
            dwThreadID = GetWindowThreadProcessId( hwndTray, NULL );
    }

    memset (g_SavedTrayData, '\0', MAX_SAVEDTRAYDATA * sizeof (COPYDATASTRUCT));
    g_hHook = SetWindowsHookEx( WH_CALLWNDPROCRET, TrayWndProc, GetModuleHandle( "geOTraySpy.dll" ), dwThreadID );
    if (g_hHook == 0)
    {
        DWORD dw = GetLastError(); 

        char szMyString[14] ={0};
        sprintf(szMyString, "0x%08x",dw);
        //MessageBox(NULL, szMyString, TEXT("Last Error"), MB_OK) ;
    }

    
    //MessageBox(NULL, "sethook called", "", MB_OK);
    return g_hHook != 0;
}

//
// UnsetTrayHook
//
// Unsets the hook
//

bool WINAPI UnsetTrayHook ()
{
    if( UnhookWindowsHookEx( g_hHook ) )
    {
        g_hHook = NULL;
        g_hTargetWin = NULL;
        return true;
    }

    return false;
}

第 2 步:我尝试在 AutoIt 中调用它......它似乎不太喜欢它,以下是我认为它应该工作的方式:

ConsoleWrite ( "-----------------------" & @CRLF )
ConsoleWrite ( "[#] Opening " & @ScriptDir & "\geOTraySpy.dll..." & @CRLF )
$libGeotrayspy = DllOpen( @ScriptDir & "\geOTraySpy.dll" )
    If $libGeotrayspy = -1 Then ConsoleWrite ( "    [!] Fail!" & @CRLF )
ConsoleWrite ( "[#] Registering function 'SetTrayHook'..."& @CRLF )

$win = GUICreate("Test")

$resGeotrayspyHook = DllCall( $libGeotrayspy, "BOOLEAN", "SetTrayHook", "str", "AutoIt v3 GUI" )
    If @error Then ConsoleWrite ( "    [!] Fail!" & @CRLF )
ConsoleWrite ( "[#] 'SetTrayHook' Result: '..."& $resGeotrayspyHook[0] & @CRLF )

GUIRegisterMsg(0x4A,"SomeFunction")
GUISetState()

while 1
    Sleep(100)
WEnd

func SomeFunction($hWndGUI, $MsgID, $WParam, $LParam)
    MsgBox(0,"WM_COPY sent","")
EndFunc

您可能想亲自询问一些更有经验的 Win API 脚本家伙,有一篇值得注意的帖子:

http://www.autoitscript.com/forum/index.php?showtopic=110231 指出,我们尝试做的事情目前可能在 AutoIt 中不起作用......Wraithdu 和 Kafu 似乎有点搞砸了,你可以试试......

但是,我可以验证DLL是否有效,因为我已通过以下代码在C#中进行了测试:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace HookTest
{
    public partial class Form1 : Form
    {
        [DllImport("geOTraySpy.dll")]
        private static extern bool SetTrayHook(string szServerName);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string className = GetCurrentClassName();
            bool res = SetTrayHook(className);
            MessageBox.Show(res.ToString());
        }
        protected override void WndProc(ref Message m)
        {
            Debug.WriteLine(m.ToString());
            base.WndProc(ref m);
        }
        protected string GetCurrentClassName()
        {
            StringBuilder className = new StringBuilder(255);
            GetClassName(this.Handle, className, 255);
            return className.ToString();
        }
    }

}

五、通知图标信息的迁移策略

(本节于 2024.05.16 补充)

Win 11 目前在积极实施针对系统托盘框架的迁移策略,为了适应现代操作系统的 UI 设计,他们推出了使用 UWP/Xaml 编写的新式任务栏,过渡阶段发生在 Win11 22H2 。目前,系统托盘通知图标的信息将采用新的内存结构和物理结构存储。

在对通知图标物理结构的研究中,我们发现如下注册表位置存储结构化信息。

HKEY_CURRENT_USER\Control Panel\NotifyIconSettings

在该位置下的键值具有特殊的含义。例如:Version 表示版本号为 3 ,UIOrderList 则是图标在 UI 列表中的顺序。MigrationStatus 表示图标存储的迁移状态,这是因为在以前使用 IconStream 存储此类信息,自新版本开始,则会迁移到新的结构而废弃原来的模式。

为确认 UIOrderList 是控制通知图标顺序列表的存储结构,使用 Procmgr 监视 explorer 对该注册表值项的更改。设置如下:

尝试拖动托盘图标的位置,会触发注册表更改:

使用监视器 API 监视注册表更改:

#include <windows.h>
#include <iostream>
#include <iomanip>

#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383

void PrintHexDump(const BYTE* pData, DWORD dwSize, const BYTE* pPreviousData = nullptr, DWORD previousDataSize = 0, bool highlightChanges = false) {
    const int bytesPerLine = 16;
    for (DWORD i = 0; i < dwSize; i += bytesPerLine) {
        std::cout << std::setw(8) << std::setfill('0') << std::hex << i << "  ";
        for (int j = 0; j < bytesPerLine; ++j) {
            if (i + j < dwSize) {
                if (highlightChanges && pPreviousData && i + j < previousDataSize && pPreviousData[i + j] != pData[i + j]) {
                    // Print in red if the byte is different
                    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED);
                }
                std::cout << std::setw(2) << std::setfill('0') << std::hex << static_cast<int>(pData[i + j]) << " ";
                SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
            }
            else {
                std::cout << "   ";
            }
        }
        std::cout << " ";
        for (int j = 0; j < bytesPerLine && i + j < dwSize; ++j) {
            char c = pData[i + j];
            if (c >= 32 && c <= 126) {
                if (highlightChanges && pPreviousData && i + j < previousDataSize && pPreviousData[i + j] != pData[i + j]) {
                    // Print in red if the byte is different
                    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED);
                }
                std::cout << c;
                SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
            }
            else {
                std::cout << ".";
            }
        }
        std::cout << std::endl;
    }
}

void MonitorRegistryChanges(HKEY hKey, LPCWSTR subkey) {
    LONG lRes;
    HKEY hRegKey;
    DWORD dwFilter = REG_NOTIFY_CHANGE_LAST_SET;
    HANDLE hEvent;
    BYTE previousData[MAX_VALUE_NAME];
    DWORD previousDataSize = sizeof(previousData);
    bool firstTime = true;

    lRes = RegOpenKeyEx(hKey, subkey, 0, KEY_NOTIFY | KEY_READ, &hRegKey);
    if (lRes != ERROR_SUCCESS) {
        std::cerr << "Error opening registry key." << std::endl;
        return;
    }

    hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (hEvent == NULL) {
        std::cerr << "Error creating event object." << std::endl;
        RegCloseKey(hRegKey);
        return;
    }

    while (true) {
        lRes = RegNotifyChangeKeyValue(hRegKey, TRUE, dwFilter, hEvent, TRUE);
        if (lRes != ERROR_SUCCESS) {
            std::cerr << "Error monitoring registry key." << std::endl;
            CloseHandle(hEvent);
            RegCloseKey(hRegKey);
            return;
        }

        WaitForSingleObject(hEvent, INFINITE);

        // Registry key has changed, now read the new value
        BYTE newData[MAX_VALUE_NAME];
        DWORD newDataSize = sizeof(newData);
        lRes = RegQueryValueEx(hRegKey, L"UIOrderList", NULL, NULL, newData, &newDataSize);
        if (lRes == ERROR_SUCCESS) {
            if (!firstTime) {
                // Print the previous and new value with highlighted changes
                std::cout << "Previous value:" << std::endl;
                PrintHexDump(previousData, previousDataSize, nullptr, 0, true);
                std::cout << "New value:" << std::endl;
                PrintHexDump(newData, newDataSize, previousData, previousDataSize, true);
            }
            else {
                firstTime = false;
            }
            // Save the current value for comparison next time
            std::memcpy(previousData, newData, newDataSize);
            previousDataSize = newDataSize;
        }
        else {
            std::cerr << "Error reading registry value." << std::endl;
        }
    }

    CloseHandle(hEvent);
    RegCloseKey(hRegKey);
}

int main() {
    HKEY hKey = HKEY_CURRENT_USER;
    LPCWSTR subkey = L"Control Panel\\NotifyIconSettings";

    MonitorRegistryChanges(hKey, subkey);

    return 0;
}

同表交换顺序(红色为改变的部分,此内容是一个图标被移动到后一个图标的位置上,原本的后一个图标往前移动一位):

异表交换(由溢出通知区域和升级通知区域之间切换):

通过对其中一次变化的记录可以确定结构单元的大小。如图所示,将 8 字节对齐的一段倒序转为 10 进制:

可以发现这个数值很熟悉:

可以发现结果和实际的 TRAYNOTIFYICONID 对应。

所以,UIOrderList 控制通知图标的顺序列表,列表中记录着图标的 TRAYNOTIFYICONID 按照小端序存储的结果,NotifyIconSettings 的子键名称和这个值对应。顺序列表的先后层次是:溢出通知区图标顺序、系统升级通知栏图标顺序、用户通知区域图标顺序。研究将进一步深化对 UIOrderList 的解析,以便于辅助开发托盘图标的整理功能。因为目前发现,修改注册表的方法不能立即生效,需要重启 explorer 进程,但是手动移动的方法却能够立即生效。内存中一定有存储,只是目前没有找到。

已经通过堆栈回溯知道读写 UIOrderList 注册表值项的模块是 C:\Windows\System32\Taskbar.dll。

已知 系统设置程序可以修改图标的显示级别,这可能包含了修改图标顺序的一部分直接接口,并且可以立即更新,这引起了我的注意。


下面的内容则是《最新方案(二)》中已经介绍过的内容,现在我补充几点。

(1)IconGuid 和 UID 只能有一个有效,定义了 GUID 则 UID 无效,也不会记录在这里。

我们看看文档中的原话:

 Shell 使用 (hWnd 加 uID) 或 guidItem 来标识调用 Shell_NotifyIcon 时要针对哪个图标进行操作。 可以通过为每个 hWnd 分配不同的 uID 来让多个图标与单个 hWnd 相关联。 如果指定 了 guidItem ,则忽略 uID 。

(2)研究 IconSnapshot 的结构,它是否能够转换为 HBitmap 和 HICON ?

看上去 IconSnapshot 是图标图形的快照,从二进制结构上来看,似乎是色块的编码,具体是否和 HICON 的解析对应,更改是否生效以及生效的时机。都是有待后面仔细研究的。

(3)存在用于隐藏(扩展)图标的注册表项?

IsPromoted 是之前说过的,控制单个图标是否折叠到角溢出栏。以前是可以完全隐藏图标的,现在的隐藏图标的注册表设置暂时还没找到。


如果大家有更好的方法,欢迎继续交流。

【更新记录】

2023.11.15 发现微软可能在新的系统中修改了接口,导致 TB_BUTTON 类消息现在被 UIPI 拒绝,并且好像没法继续获取相关的结构体。

[因其他事情而搁置]

2023.12.10 根据相关论坛消息,已经确认微软删除了旧的 Shell 的 TrayBar 接口。

2024.01.01 想到在任务栏创建图标一般通过 Shell_NotifyIcon 函数,我就通过 IDA 逆向分析了一下这个函数,我们发现,它并没有实现创建窗口的过程。它将 NOTIFYICONDATAW 结构封装在 WM_COPYDATA 中,并通过 SendMessageTimeout 发送给 explorer.exe 的 Shell_TrayWnd 窗口,真正的处理应该在 Shell_TrayWnd 的消息线程中。

[春节期间开始继续分析]

2024.02.04 成功分析出发送至托盘图标的数据结构,并实现调用方的信息挂钩。

2024.02.14 成功找到 explorer 的 CTray::v_WndProc 回调函数,初步实现拦截 COPYDATA 消息,但仍然存在问题。

2024.02.15 成功实现 explorer.exe 的钩子模块,并在调试模式下实现记录所有任务栏图标信息。

2024.02.16 成功实现调试注入器,并成功在 explorer.exe 进程初始化的第一个 TLS 回调阶段注入模块。

2024.02.20 完成对该方法的原理的解释和基本实现工具。(最新方案《一、二》)

2024.02.22 完善了挂钩的方式,由 API Hook 改用 Win32 Hook(CallWndProcHook)。

                (最新方案《三》)

[工作繁忙,没时间更新]

2024.05.05 添加了 CallWndProcHook 技术方案的解释说明文本。(最新方案《三》)

2024.05.13 进一步完善最新方案(三)的解释文本

2024.05.16 提出了对新的注册表键值的进一步解释,未来将进一步深化研究

2024.05.24 近期将转入研究 任务栏图标固定 和相关注册表存储研究上,同时因为工作比较繁忙,此研究工作将经常搁置。

2024.06.01 修正代码中的文本输出错误,早期版本系统的字符串编码方式与现在不同。

未来目标:通过编程方式实现在 explorer.exe 的托盘图标信息托管,构建一个 GUI 程序。

六、参考文档

需要的可以参考下列文献或论坛资源: 

1.WM_COPYDATA for Taskbar Interface(Geoff Chappell) - 8th April 2007

        这个逆向文档要挂魔法,大佬们应该都懂,鄙人就不多说了。

2.Shell_NotifyIcon(Geoff Chappell) - 23rd January 2009

        这里面补充了一些在 NT6.0 开始的重要变化。

3.Rewrite the wrapping code for shell taskbar notifications. - Feb 4 2018

        ReactOS 一个更新文档,里面解释了他们借鉴了 Geoff Chappell 逆向的工作来跟进修复系统通知栏图标的处理逻辑。

4.SysPager 源代码 - ReactOS

5.SysPager 文档 - ReactOS 

6.NOTIFYICONDATAW (shellapi.h) - Win32 apps | Microsoft Learn

7.GitHub - katahiromz/SysNotifyHooker: API hook for Windows Explorer

8.[原创]TLS回调函数(Note)-软件逆向-看雪-安全社区|安全招聘|kanxue.com

9.TLS回调函数-CSDN博客

10.Windows 7 Notification Area Automation

11.Make Application Icon visible with Systray

[... 更多未列举]


发布于:2023-01-07;更新于:2024-02-16 / 25 、 2024-05-13/16/24、2024-06-01。

希望通过我的分析,能够给遇到相关问题的友人一点微薄的解答。文章如果有误,欢迎各位斧正,谢谢!

转载请注明博客来源:https://blog.csdn.net/qq_59075481/article/details/128594435

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
如果在Windows 11操作系统中点击了Wi-Fi、音量、电池等图标却没有反应,可能是由于以下原因之一: 1. 系统错误:可能是由于内部错误或操作系统的问题导致的。您可以尝试重新启动计算机,以解决可能的临时问题。如果问题仍然存在,请考虑更新操作系统或进行其他系统维护操作,例如运行病毒扫描或修复系统文件。 2. 驱动程序问题:这可能是由于缺少或损坏的驱动程序引起的。您可以尝试通过设备管理器来更新或重新安装相关设备的驱动程序。找到相应的设备类别,右键单击并选择“更新驱动程序”或“卸载设备”。在卸载设备后,重新启动计算机,操作系统将自动重新安装驱动程序。如果问题仍然存在,请访问设备制造商的官方网站,寻找最新的驱动程序,手动下载并安装它们。 3. 软件冲突:这可能是由于与其他程序或应用程序的冲突导致的。尝试关闭您最近安装的第三方程序,特别是那些可能与系统托盘图标相关的程序。如果问题在关闭特定程序后消失,则可以确定该程序是问题的根源。您可以尝试更新该程序,或者考虑使用其他替代方案。 总之,如果在Windows 11中点击Wi-Fi、音量、电池图标没有反应,您可以尝试重新启动计算机、更新驱动程序或解决可能存在的软件冲突。如果问题仍然存在,建议联系Windows 11技术支持团队,以获得更详细的帮助和解决方案。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值