获取 Windows 系统托盘图标信息的最新方案(一)

目录

前言

1 获取系统托盘图标的一般方法

1.1 使用 TB_ 类消息的注意事项

1.2 代码编写和测试

1.3 技术的适用范围

2 深度分析系统托盘图标信息

2.1 分析 Shell_NotifyIcon 函数参数

2.2 分析 Shell_NotifyIcon 函数的内部细节

2.3 分析 WM_COPYDATA 消息

2.4 调用方挂钩拦截消息封送过程

2.5 接收方挂钩实现获取任意图标信息

3 注入 HOOK 模块的方案

3.1 创建挂起进程注入

3.2 创建调试进程注入

3.3 等待调试事件

3.4 处理调试事件

3.5 完整代码和测试结果

3.6 补充说明

4 初步分析系统托盘图标的注册表转储

4.1 传统的注册表转储信息

4.2 Win11 特有的转储信息


本文出处链接:[https://blog.csdn.net/qq_59075481/article/details/136134195]

前言

本文分析获取系统通知区域图标的多种方法。解释了在 Win11 22H2 更新后无法获取托盘图标信息的问题所在,并给出了有效的解决方案。这篇文章发布前,我一直在我的一篇历史文章中更新我的研究进度。在解决这个这个问题的过程中,我逐步地了解到了通知消息传递的一些细节,而在此之前对细节实现的分析过程只是被我潦草地堆砌在一起,并没有很好地梳理事情的整个流程。所以我决定将文章重新整理出来。也就是大家现在看到的这篇长文,请认真地阅读下去,这将会解决一部分的一直以来在文献中含糊不清的问题。如果你不了解系统通知方面的知识,建议你先阅读第 5 篇 “对通知区域(Win 系统托盘)窗口层次的分析” 以便于了解设计基础知识。

提示:这里说一下这个系列主要部分的区别。首先《尝试》篇相当于是研究日志,所以写的很随意。阅读时,建议先看最新方案(一)和(三)的部分。其中,最新方案(一)扩展了本文截至 2024.02 月所更新内容的解释,最新方案(三)是对所采用方法的一个改进方法。警告:由于本人工作繁忙,本项目研究将长期缓慢更新甚至搁置!

系列文章列表:

编号文章标题AID
1

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

128594435
2获取 Windows 系统托盘图标信息的最新方案(一)136134195
3获取 Windows 系统托盘图标信息的最新方案(二)136199670
4获取 Windows 系统托盘图标信息的最新方案(三)136240462
5对通知区域(Win 系统托盘)窗口层次的分析128538050

P.S.: 第 3 篇文章主要解释本篇第 4 节的代码原理,第 4 篇文章是改进了挂钩处理的方式。

1 获取系统托盘图标的一般方法

在 Windows 操作系统上,一般情况下我们可以通过 TB_BUTTONCOUNT 消息来获取 ToolBar 的按钮个数。传统的任务栏通知区域(俗称系统托盘)就采用了 Toolbar 的设计,所以我们可以通过发送 TB_BUTTONCOUNT 来获取通知图标的个数。获取到个数后,就可以通过 TB_GETBUTTONTEXT 来获取 Toolbar 上图标的显示文本信息,但调用它的前提是使用 TB_GETBUTTON 消息。这个方法从 WinXP 以来是一直在使用的。

关于该方法的原理前面已经有人整理过了,可以看这一系列文章:

1.1 使用 TB_ 类消息的注意事项

注意:下面的 UI 设计理念可能和微软在较新系统上的设计理念不相同。

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

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

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

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

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

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

1.2 代码编写和测试

下面我们简单谈一谈代码的编写,网络上一种用于 Win7-Win11 的代码如下(有缺陷):

#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 10 的需求,我曾对代码做了改进(代码是以前临时写的,有点丑)

#include <windows.h>
#include <string>
#include <commctrl.h>
#include <atlstr.h>
#include <Shlobj.h>
#include <locale.h>

int RepeatedHwndscout;
HWND h_oldHwnd = NULL;
HWND Lasterrorhwnd = NULL;

// 判断 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;
}

// 获取系统升级的通知区域句柄
HWND FindSysUpdateNotifyWindow() {

	HWND hWnd = NULL;

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

	return hWnd;
}


/*
* 已知路径转换函数
*
* 例如:{F38BF404-1D43-42F2-9305-67DE0B28FC23} 表示 C:\Windows;
* {F38BF404-1D43-42F2-9305-67DE0B28FC23}\explorer.exe 可以合并转换为 C:\Windows\explorer.exe

* {F38BF404-1D43-42F2-9305-67DE0B28FC23}  // SystemRoot:Windows Folder
* {1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}  // SystemRoot:Windows\\System32 Folder
* {7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}  // SystemRoot:Program Files (x86) Folder
* {6D809377-6AF0-444B-8957-A3773F02200E}  // SystemRoot:Program Files Folder
*/

HRESULT RestructureDataFilePath(const wchar_t* strGuid, wchar_t** strBinPathAffer) {

	GUID stGuid = { 0 };
	WCHAR* strBuffer = nullptr;
	size_t wcsBufferlen = 0;
	HRESULT str2sidreslt = CLSIDFromString((LPCOLESTR)strGuid, (LPCLSID)&stGuid);

	if (str2sidreslt == (HRESULT)NOERROR) {

		// 查询已知文件夹路径

		str2sidreslt = ::SHGetKnownFolderPath(stGuid, KF_FLAG_DONT_VERIFY, NULL, &strBuffer);

		if (SUCCEEDED(str2sidreslt)) {
			wcsBufferlen = wcslen(strBuffer) * sizeof(WCHAR) + sizeof(WCHAR);
			*strBinPathAffer = (WCHAR*)malloc(wcsBufferlen);
			memset(*strBinPathAffer, 0, wcsBufferlen);
			memcpy(*strBinPathAffer, strBuffer, wcsBufferlen);
			CoTaskMemFree(strBuffer);// 释放内存
			return NOERROR;
		}
		else {
			printf("SHGetKnownFolderPathFailed,errorCode = [%d]\n", GetLastError());
			return str2sidreslt;
		}
	}
	else
		return str2sidreslt;
}

// 遍历窗口
BOOL EnumNotifyWindow(HWND hWnd)
{
	// 获取托盘进程ID
	DWORD dwProcessId = 0;
	// 初始化
	DWORD dw_addr_dwData = 0;
	BYTE buff[1024] = { 0 };
	WCHAR* ws_filePath = nullptr, filepathsuffix[MAX_PATH] = { 0 };
	WCHAR* ws_tile = nullptr;
	WCHAR KnownfoldID[39] = { 0 };
	wchar_t* strBinPathAffer = nullptr;
	HWND h_mainWnd = NULL;
	TCHAR szWndClassName[256] = { 0 },
		toolBarTitle[256] = { 0 },
		toolBarClass[256] = { 0 };
	const char* delimiter = "\\";
	char filepath[256]{};
	char* szExeFile = nullptr;
	char* pBufferContext = nullptr;
	char szWndTitleName[1024] = { 0 };
	char shortHwnds[64] = { 0 };

	int i_data_offset = 12;
	int i_str_offset = 18;

	GetWindowThreadProcessId(hWnd, &dwProcessId);
	if (dwProcessId == 0) {
		printf("-------------------------------------------------------------------------------------\n");
		printf("GetWindowThreadProcessId failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
		printf("-------------------------------------------------------------------------------------\n");
		Lasterrorhwnd = hWnd;
		return FALSE;
	}

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

	if (hProcess == nullptr) {
		printf("-------------------------------------------------------------------------------------\n");
		printf("OpenProcess failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
		printf("-------------------------------------------------------------------------------------\n");
		Lasterrorhwnd = hWnd;
		return FALSE;
	}

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

	if (p_tbbutton == nullptr) {
		printf("VirtualAllocEx failed: %d.\n", GetLastError());
		Lasterrorhwnd = hWnd;
		return FALSE;
	}

	/*
		typedef struct _TBBUTTON {
			int iBitmap;				// 索引
			int idCommand;				// 与按钮关联的命令标识符
			BYTE fsState;				// 按钮状态
			BYTE fsStyle;				// 按钮风格
		#ifdef _WIN64
			BYTE bReserved[6];          // 对齐
		#elif defined(_WIN32)
			BYTE bReserved[2];          // 对齐
		#endif
			DWORD_PTR dwData;			// 存放程序自定义数据
			INT_PTR iString;			// 存放信息字符
		} TBBUTTON, NEAR* PTBBUTTON, *LPTBBUTTON;
	*/

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

	// 获取托盘图标个数
	int i_buttons = 0;
	i_buttons = (int)SendMessage(hWnd, TB_BUTTONCOUNT, 0, 0);
	if (i_buttons == 0) {
		printf("-------------------------------------------------------------------------------------\n");
		printf("TB_BUTTONCOUNT message failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
		printf("-------------------------------------------------------------------------------------\n");
		Lasterrorhwnd = hWnd;
		return FALSE;
	}

	// 获取通知区域主窗口的标题和窗口类名称

	GetWindowTextW(hWnd, toolBarTitle, sizeof(toolBarTitle) / sizeof(TCHAR));
	GetClassNameW(hWnd, toolBarClass, sizeof(toolBarClass) / sizeof(TCHAR));

	// 输出标题 & 窗口类名称
	printf("=====================================================================================\n");
	printf("Tray_ToolBar WindowName = %ws | Tray_ToolBar ClassName = %ws\n", toolBarTitle, toolBarClass);
	printf("ChildTray PointsNumber = %d.\n\n", i_buttons);

	// 遍历通知区域图标信息

	for (int i = 0; i < i_buttons; i++) {

		// 获取 TBBUTTON 结构体指针

		if (!SendMessage(hWnd, TB_GETBUTTON, i, (LPARAM)p_tbbutton)) {
			printf("-------------------------------------------------------------------------------------\n");
			printf("TB_GETBUTTON message failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
			printf("-------------------------------------------------------------------------------------\n");
			Lasterrorhwnd = hWnd;
			return FALSE;
		}

		// 读 TBBUTTON.dwData(附加信息)

		if (!ReadProcessMemory(hProcess, (LPVOID)((DWORD)p_tbbutton + i_data_offset), &dw_addr_dwData, 4, 0)) {
			printf("-------------------------------------------------------------------------------------\n");
			printf("ReadProcessMemory failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
			printf("-------------------------------------------------------------------------------------\n");
			Lasterrorhwnd = hWnd;
			return FALSE;
		}

		// 读取结构体信息

		if (dw_addr_dwData) {
			if (!ReadProcessMemory(hProcess, (LPCVOID)dw_addr_dwData, buff, 1024, 0)) {
				printf("-------------------------------------------------------------------------------------\n");
				printf("ReadProcessMemory failed: %d \tFrom hwnd: %I64X.\n", GetLastError(), (UINT64)hWnd);
				printf("-------------------------------------------------------------------------------------\n");
				Lasterrorhwnd = hWnd;
				return FALSE;
			}

			h_mainWnd = (HWND)(*((DWORD*)buff));
			ws_filePath = (WCHAR*)buff + i_str_offset;
			ws_tile = (WCHAR*)buff + i_str_offset + MAX_PATH;

			/*
			* 排除重复记录的窗口
			*/

			if (h_oldHwnd == h_mainWnd) {
				printf("Rank(Limited)=%d; HWND 0x%I64X: MultipleRegisted once.\n", i + 1, (UINT64)h_mainWnd);
				RepeatedHwndscout += 1;
				continue;
			}
			else {
				printf("----------------\n");
				printf("Tray Rank = %d: \n", i + 1);
			}

			printf("MainWindowHandle = 0x%I64X\n", (UINT64)h_mainWnd);

			sprintf_s(filepath, "%ws", ws_filePath);
			szExeFile = strtok_s(filepath, delimiter, &pBufferContext);
			while (szExeFile)
			{
				size_t len = strlen(szExeFile);
				if (szExeFile[len - 1] == 101 && szExeFile[len - 2] == 120 && szExeFile[len - 3] == 101) {
					printf("PatrilinealProcessName = %s\n", szExeFile);
				}
				szExeFile = strtok_s(NULL, delimiter, &pBufferContext);
			}

			/*
			* 
			* 判断是否是系统已知文件路径,如果是就转换为可读路径
			* 
			*/

			// 初始化内存操作
			wmemset(KnownfoldID, 0, sizeof(KnownfoldID) / sizeof(wchar_t));
			wmemset(filepathsuffix, 0, sizeof(filepathsuffix) / sizeof(wchar_t));
			
			// GUID 字符串的特征定位操作,建议使用 C++ 的 std::string 操作

			if (ws_filePath[0] == L'{' && ws_filePath[37] == L'}' ) {

				for (int i = 0; i < 38; i++) {
					KnownfoldID[i] = ws_filePath[i];
				}
				
				for (int j = 38; j < wcslen(ws_filePath); j++) {
					filepathsuffix[j - 38] = ws_filePath[j];
				}

				// 查询已知路径字符串的 Win32 路径

				HRESULT lpresult = RestructureDataFilePath(KnownfoldID, &strBinPathAffer);

				printf("ExecuteFileBinPath = %ws%ws\n", strBinPathAffer, filepathsuffix);
				free(strBinPathAffer);
			}
			else {
				printf("ExecuteFileBinPath = %ws\n", ws_filePath);
			}

			if (!ws_tile[0]) {
				printf("WindowTitle = (None)\n");
			}
			else {
				bool bEnterFlag = false;
				sprintf_s(szWndTitleName, "%ws", ws_tile);

				// 查询并去除 szTip 字符串中的换行符号,
				// BUG:对中文处理可能存在问题,建议使用 C++

				for (int index = 1; index < strlen(szWndTitleName); index++) {
					if (szWndTitleName[index] == '\n') {
						// printf("Title have Enter at str[%d].\n", index);
						szWndTitleName[index] = ';';
						bEnterFlag = true;
					}
				}

				if (bEnterFlag) {
					printf("WindowTitle = %s \n", szWndTitleName);
				}
				else {
					wprintf(L"WindowTitle = %ws\n", ws_tile);
				}

			}
		}

		// 输出窗口类的名称字符串,如果不为空字符串的话

		if (h_mainWnd) {
			GetClassNameW(h_mainWnd, szWndClassName, sizeof(szWndClassName) / sizeof(TCHAR));// 函数调用
			printf("WindowClassName = %ws\n", szWndClassName);// 控制台的输出窗口类的名称
		}
		else {
			printf("WindowClassName = (None)\n");
		}

		// 判断窗口是否可见

		if (h_mainWnd != NULL && IsWindowVisible(h_mainWnd)) {
			printf("IsWindowVisible: True.\n");
		}
		else {
			printf("IsWindowVisible: False.\n");
		}

		// 获取窗口矩形的相关信息

		RECT rect = { 0,0,0,0 };
		if (h_mainWnd != NULL && !GetWindowRect(h_mainWnd, &rect)) {
			printf("Failed to Find Window Rect.ecode = %d\n", GetLastError());
		}
		else {
			int wid = rect.right - rect.left;
			int heit = rect.bottom - rect.top;
			if (!wid && !heit) {
				printf("Window rectangle size is Zero.\n");
			}
			else {
				printf("WindowRect: \n");
				printf(" left \t top \tright\tbottom\t\n");
				printf("  %d\t%d\t%d\t%d\t\n", rect.left, 
					rect.top, rect.right, rect.bottom);
			}

			if (!rect.right && !rect.left && !rect.bottom && !rect.top) {
				printf("The four corners of window are all with Zero-Pos.\n");
			}
			else {
				printf("Position: \n");
				printf("\t(%d,%d) - (%d,%d)\n", rect.left, 
					rect.top, rect.right, rect.bottom);
				printf("Rect: %d x %d.\n", wid, heit);
			}
		}


		// 记录遍历的窗口句柄
		h_oldHwnd = h_mainWnd;

		// 清理内存
		dw_addr_dwData = 0;
		h_mainWnd = NULL;
		ws_filePath = 0;
		ws_tile = NULL;
		filepath[0] = '\0';
		szWndClassName[0] = '\0';
		toolBarTitle[0] = '\0';
		toolBarClass[0] = '\0';
		printf("\n");
	}


	if (VirtualFreeEx(hProcess, p_tbbutton, 0, MEM_RELEASE) == 0) {
		printf("-------------------------------------------------------------------------------------\n");
		printf("VirtualFreeEx failed: %d \tFrom hwnd: %I64X\n", GetLastError(), (UINT64)hWnd);
		printf("-------------------------------------------------------------------------------------\n");
		Lasterrorhwnd = hWnd;
		return FALSE;
	}


	if (CloseHandle(hProcess) == 0) {
		printf("-------------------------------------------------------------------------------------\n");
		printf("CloseHandle failed: %d \tFrom hwnd: %I64X\n", GetLastError(), (UINT64)hWnd);
		printf("-------------------------------------------------------------------------------------\n");
		Lasterrorhwnd = hWnd;
		return FALSE;
	}


	if (RepeatedHwndscout != 0) {
		printf("Skipped %d Windows.\n", RepeatedHwndscout);
		printf("The number of Tray Notification Windows under this Notifi-hWnd should actually be %d, not %d.\n",
			i_buttons - RepeatedHwndscout, i_buttons);
	}

	return TRUE;
}

int main()
{
	system("cls");

	// 控制台中文支持
	setlocale(LC_ALL, ".utf8");

	// 获取通知区域窗口的句柄
	HWND h_tray = FindTrayWnd();
	HWND h_tray_fold = FindNotifyIconOverflowWindow();
	HWND h_sys_up = FindSysUpdateNotifyWindow();

	// 枚举各个窗口通知区域图标信息

	bool t1 = EnumNotifyWindow(h_tray);
	bool t2 = EnumNotifyWindow(h_tray_fold);
	bool t3 = EnumNotifyWindow(h_sys_up);

	// 判断遍历结果

	if (t1 == false || t2 == false || t3 == false) {
		if (Lasterrorhwnd == h_sys_up) {
			printf("===============================================================================================\n");
			printf("\t\t\tUpdateNotifyWindow - System, has none Icon now.\t\t\t\n");
			printf("===============================================================================================\n");
		}
		else {
			printf("EnumNotifyWindow false.ObjWindowHandle = %I64X.\n", (UINT64)Lasterrorhwnd);
		}
	}

	printf("Press any key to CLOSE the console.");

    ::getchar();
	return 0;
}

代码修复了已知路径未将 GUID 或 UUID 转换为完整路径,Taskmgr 重复窗口记录、窗口是否可见等问题。运行效果如下:

Win10 测试运行结果截图

1.3 技术的适用范围

TBBUTTON 方法自 Win11 22H2 Build 22621.1344 和 22621.1413 (正式) 起不再有效。微软修复了加载系统托盘图标相关的问题,因为该问题可能会导致 explorer.exe 卡顿甚至崩溃。就是这一次修复其实微软顺便砍掉了很多冗余的东西,比如像 shell:::{05d7b0f4-2121-4eff-bf6b-ed3f69b894d9} 指向的旧版的通知区域图标管理页面,一些注册表设置等。

现在你只能看到空白的通知区域图标管理页面

在这种情况下,TB_BUTTONCOUNT 应该会失败,并返回 0,并且最近一次错误是 “拒绝访问”,这也是国内、国外论坛在讨论的重点。

但是,资源管理器的图标管理是否存在其他接口,图标的数据是怎么组织的?管理接口肯定是存在的,只不过没有公开。图标数据也肯定不是一直放在内存中的,因为重启后设置里面依然记录着已知的图标信息,即使它们曾经启用过而现在对应的进程并未活动。下面我们将逐步揭开 Explorer Tray 的神秘的面纱。

2 深度分析系统托盘图标信息

系统通知区域图标的创建需要依赖一些接口函数,通过分析这些接口函数,我们就能够对资源管理器的通知栏图标有更加多维的理解。

2.1 分析 Shell_NotifyIcon 函数参数

托盘图标目前只能通过微软提供的 Shell_NotifyIcon 函数来管理,那么我们研究的切入点也就是这个函数。

 Shell_NotifyIcon 函数的声明和参数的部分解释如下:

MSDN 称该函数是只将消息发送到任务栏的通知区域的一个函数。这说明了很可能该函数不是实现图标的全部功能,而是将特定的窗口消息发送给 explorer 进程。

函数声明:

BOOL Shell_NotifyIconW(
        DWORD                          dwMessage,
        PNOTIFYICONDATAW   lpData
);

一个 DWORD 数值,该值指定要由此函数执行的操作。 可以具有下表列出的任意数值之一:

dwMessage 参数功能
dwMessage 支持的数值表示的功能

NIM_ADD

(0x00000000)

将图标添加到状态区域。

NIM_MODIFY

(0x00000001)

修改状态区域中的图标。 

NIM_DELETE

(0x00000002)

从状态区域中删除图标。

NIM_SETFOCUS

(0x00000003)

将焦点返回到任务栏通知区域。

NIM_SETVERSION

(0x00000004)

指示通知区域按照 lpdata 所指向结构的 uVersion 成员中指定的版本号的行为。 版本号指定可识别的成员。

第二个参数 lpData 为指向 NOTIFYICONDATA 结构的指针。 结构的内容取决于 dwMessage 的值。它可以定义要添加到通知区域的图标、使该图标显示通知,或标识要修改或删除的图标。

NOTIFYICONDATA 结构体有 Unicode 和 ANSI 别名表示的不同版本。其次, MSDN 上给出的是简化版,实际上和 SDK 中的定义不完全一致。下面摘录的是 MSDN 的简化版:

typedef struct _NOTIFYICONDATAW {
  DWORD cbSize;
  HWND  hWnd;
  UINT  uID;
  UINT  uFlags;
  UINT  uCallbackMessage;
  HICON hIcon;
#if ...
  WCHAR szTip[64];
#else
  WCHAR szTip[128];
#endif
  DWORD dwState;
  DWORD dwStateMask;
  WCHAR szInfo[256];
  union {
    UINT uTimeout;
    UINT uVersion;
  } DUMMYUNIONNAME;
  WCHAR szInfoTitle[64];
  DWORD dwInfoFlags;
  GUID  guidItem;
  HICON hBalloonIcon;
} NOTIFYICONDATAW, *PNOTIFYICONDATAW;

关于各个成员的解释我就不过多介绍了,可以直接看 MSDN 的文档:

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

2.2 分析 Shell_NotifyIcon 函数的内部细节

下面我们需要对 Shell_NotifyIcon 函数做进一步的分析。从 NT5.0 开始,Shell_NotifyIcon 函数一直是 Shell32.dll 的导出函数。

首先,我们通过 IDA Pro 对该函数进行静态分析。该函数查找了 Shell_TrayWnd 窗口的句柄,并且如果该窗口不存在(比如资源管理器未启动),则立即返回失败信息:

判断任务栏是否已经创建

 Shell_TrayWnd 窗口相信对很多人来说并不陌生,它是任务栏父窗口,在以前的 SendMessage 方法中就涉及到从它的子窗口中获取通知区域窗口句柄的过程。下图展示了 Win 11 的任务栏窗口层次结构:

任务栏窗口层次结构

网上的代码大多数情况下忽略了 TrayNotifyWnd 下的“系统升级通知窗口”的图标,我将这个区域称为临时区域,是因为它默认情况下窗口大小为 0,在 Win 7 到 Win11 更新之前的版本上,该区域为程序刚刚创建系统托盘图标时候,微软会将该图标显示在临时通知区域(Win11 更新过后似乎不会自动放到这个区域),它位于系统通知区域和任务栏溢出区域角标之间。具体如下图所示:

Win10 系统通知区域分块

最左侧的是溢出栏角标,中间新增的就是临时通知栏,右侧是系统常驻通知区域。系统会将新创建的图标放在这个可变大小的临时通知区域中,时间长达 1 分钟,随后根据配置文件,将该图标从临时区域移动到溢出通知区域或者常驻通知区域。

这也就解释了我在 1.2 的改进版代码中增加了对系统升级通知区域(临时通知区域)的解析的原因:

// 获取系统升级的通知区域句柄
HWND FindSysUpdateNotifyWindow() {

	HWND hWnd = NULL;

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

	return hWnd;
}

当然,上面谈的这些只适用于 10.0.22621.1413 之前的发布版本系统,对于最新的系统,已经删除了窗口对 TBBUTTON 消息处理的支持。下面我们继续分析 Shell_NotifyIcon 函数。

从 NT 5 (XP) 以来进行了一些功能性更改,导致该函数支持的结构体大小不唯一。为了兼容旧版本的程序,微软提供了根据版本号重构的多个结构体。所以接下来函数对结构体参数的大小进行了判断,以便于区分版本。

通过 cbSize 成员解析结构体大小,并对 dwMessage,uFlags 等进行参数校验

这一部分其实 MSDN 文档中有记录。

文档中写道:必须使用结构的大小初始化结构。如果使用当前定义的结构的大小,则应用程序可能不会使用早期版本的 Shell32.dll 运行,这需要更小的结构。

通过适当设置 NOTIFYICONDATA 结构的大小,可以保持应用程序与所有 Shell32.dll 版本的兼容性,同时仍使用当前头文件。 在初始化结构之前,使用 DllGetVersion 确定系统上安装的 Shell32.dll 版本,并使用以下值之一初始化 cbSize :

Shell32.dll 版本cbSize
6.0.6 或更高版本 (Windows Vista 及更高版本)sizeof (NOTIFYICONDATA)
6.0 (Windows XP)NOTIFYICONDATA_V3_SIZE
5.0 (Windows 2000)NOTIFYICONDATA_V2_SIZE
低于 5.0 的版本NOTIFYICONDATA_V1_SIZE

可以看出在 Vista 之前使用常量定义结构体的大小,在 Vista 之后直接定义 NTDDI_VERSION 宏并使用结构的大小 sizeof (NOTIFYICONDATA) 即可。

随后,将结构体数据拷贝到新分配的内存上(参数重组,这里的 IDA 分析有部分问题):

准备要发送的数据

对字符串参数的处理(这一部分没有仔细去分析):

字符串参数处理

随后,开始解析 dwMessage 参数,因为该参数决定了要对通知区域的具体操作:

解析 dwMessage 参数

如果 dwMessage == NIM_SETFOCUS 则会调用 AllowSetForegroundWindow 允许 explorer 设置前台窗口。

在 dwMessage 不是 NIM_SETFOCUS 的情况下,则会首先检查调用方进程的消息线程是否注册了 TaskbarCreated 这个系统保留的消息字符串,该消息在任务栏创建时候会由 explorer 向全部的顶级窗口广播,用于在任务栏重建时候允许调用方重新请求创建通知区域图标。

如果 dwMessage 是 NIM_MODIFY 则跳转到 LABEL_22 处,不是则跳转到 LABEL_23 处。如果 dwMessage 是 NIM_ADD 则继续向下执行而不跳转。从中可以看出,这里可以解释为下面的伪代码结构:

switch(dwMessage)
{
case NIM_ADD:
{
    // 添加通知区域图标
}
break;
case NIM_MODIFY:
{
    // 修改通知区域图标信息
}
break;
case NIM_DELETE:
{
    // 删除通知区域图标
}
break;
case NIM_SETFOCUS:
{
    // 返还任务栏焦点
}
break;
case NIM_SETVERSION:
{
    // 设置通知版本
}
break;
default:
    // 默认错误处理
    break;
}

如果是 NIM_ADD 则会首先执行下面的过程:

 NIM_ADD 首先执行图中的过程

通过对 lpData->hWnd 也就是图标绑定的窗口句柄调用 GetWindowThreadProcessId 来获取对应的进程 ID。再使用 OpenProcess 以 PROCESS_QUERY_LIMITED_INFORMATION 权限获取进程的访问句柄,因为后面调用 QueryFullProcessImageNameW 获取进程完整的路径必须指定  PROCESS_QUERY_LIMITED_INFORMATION 权限或者 PROCESS_QUERY_INFORMATION 权限的访问句柄。而像 SYSTEM 进程等完整性较高的进程,普通进程可能无法获取到 PROCESS_QUERY_INFORMATION 访问权限,只能获取到 PROCESS_QUERY_LIMITED_INFORMATION 权限。所以理所当然这里用的是受限访问权限。

当然这个过程是可能失败的,一方面是窗口句柄可能无效,另一方面是进程的访问和路径获取可能失败。如果路径访问失败,则错误代码是 0x80004005(LRESULT 错误代码),也就是 E_FAIL 未知故障的意思,这个错误代码是十分常见的。

句柄无效的处理

那么接下来,你肯定会想到,万一是 hWnd 指向无效窗口导致访问路径失败呢?那我们也不一定需要抛出失败啊,只需要尝试默认窗口即可了啊?该函数确实也是这么做的:

检查路径的替代品

此时,会尝试通过 GetCurrentProcessId 获取调用方进程的 PID,随后通过 SHExePathFromPid 函数来获取调用方进程的完整 Win32 路径。这个函数非常有意思,其实和刚刚的流程差不多,只不过进程 PID 不是 hWnd 指向的窗口的进程 PID,而是调用方进程的 PID。

获取完整可执行路经的方法

随后对于两种方式获取的路径都需要校验,GetLongPathNameW 判断是否始终为长路径,PathIsNetworkPathW 判断路径中是否不包含网络路径。这两部分就是确保路径是完整的本地路径。

下面就是一个有意思的环节了:

转换部分 KNOWNFOLDERID 前缀

这里将系统中部分已知文件夹的 KNOWNFOLDERID (GUID) 通过 XMM 寄存器传输到全局变量上。

接下来,通过 SHGetKnownFolderPath 获取 GUID 对应的已知文件夹默认路径,通过 PathCommonPrefixW 的返回值判断当前程序文件路径和已知文件夹路径是否存在公共前缀,并指示公共前缀的字符长度(v45),通过 lstrlenW 判断已知路径长度是否等于公共前缀长度来确定当前文件路径是否是位于已知文件夹路径下。

如果满足路径在已知文件夹路径下,则用已知文件夹路径的 GUID 字符串(StringFromGUID2 获取),替代实际的文件夹路径前缀。

判断文件路径前缀是否在 GUID 表中

这也是为什么上文在 1.2 的改进代码中,我增加了已知路径 GUID 转换函数(GUIDStringToPathPrefix)的原因:

/*
* 已知路径转换函数
*
* 例如:{F38BF404-1D43-42F2-9305-67DE0B28FC23} 表示 C:\Windows;
* {F38BF404-1D43-42F2-9305-67DE0B28FC23}\explorer.exe 将被转换为 C:\Windows\explorer.exe

* {F38BF404-1D43-42F2-9305-67DE0B28FC23}  // SystemRoot:Windows Folder
* {1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}  // SystemRoot:Windows\\System32 Folder
* {7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}  // SystemRoot:Program Files (x86) Folder
* {6D809377-6AF0-444B-8957-A3773F02200E}  // SystemRoot:Program Files Folder
*/

HRESULT RestructureDataFilePath(const wchar_t* strGuid, wchar_t** strBinPathAffer) {

	GUID stGuid = { 0 };
	WCHAR* strBuffer = nullptr;
	size_t wcsBufferlen = 0;
	HRESULT str2sidreslt = CLSIDFromString((LPCOLESTR)strGuid, (LPCLSID)&stGuid);

	if (str2sidreslt == (HRESULT)NOERROR) {

		// 查询已知文件夹路径

		str2sidreslt = ::SHGetKnownFolderPath(stGuid, KF_FLAG_DONT_VERIFY, NULL, &strBuffer);

		if (SUCCEEDED(str2sidreslt)) {
			wcsBufferlen = wcslen(strBuffer) * sizeof(WCHAR) + sizeof(WCHAR);
			*strBinPathAffer = (WCHAR*)malloc(wcsBufferlen);
			memset(*strBinPathAffer, 0, wcsBufferlen);
			memcpy(*strBinPathAffer, strBuffer, wcsBufferlen);
			CoTaskMemFree(strBuffer);// 释放内存
			return NOERROR;
		}
		else {
			printf("SHGetKnownFolderPathFailed,errorCode = [%d]\n", GetLastError());
			return str2sidreslt;
		}
	}
	else
		return str2sidreslt;
}

上面的代码是我去年早些时候编写的。现已经改进并整合到在 4.1 节的工具代码中,部分内容如下:

/*
* 已知路径转换函数
*
* 参数:LPCWSTR szKnownPath 包含 GUID 前缀的完整路径
*       PWSTR szWin32FilePath 返回 Win32 完整路径
*
* ********************************************************************************************
* 备注:
*
* {F38BF404-1D43-42F2-9305-67DE0B28FC23} 表示 C:\Windows
* {F38BF404-1D43-42F2-9305-67DE0B28FC23}\explorer.exe 将被转换为 C:\Windows\explorer.exe
*
* SystemRoot = "{F38BF404-1D43-42F2-9305-67DE0B28FC23}";//SystemRoot:Windows Folder
* System32 = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}";//SystemRoot:Windows\\System32 Folder
* Program86 = "{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}";//SystemRoot:Program Files (x86) Folder
* Program = "{6D809377-6AF0-444B-8957-A3773F02200E}";//SystemRoot:Program Files Folder
*
* ********************************************************************************************
*/
HRESULT FilePathFromKnownPrefix(
    LPCWSTR szKnownPath,
    PWSTR* szWin32FilePath
)
{

    GUID stGuid = { 0 };
    PWSTR strPathPrefix = nullptr;
    PWSTR strWin32Path = nullptr;
    HRESULT str2GuidReslt = E_FAIL;
    std::wstring wsKnownPath;
    size_t nPos = std::string::npos;
    size_t nPrefix = 0,
        nKnownPath = 0,
        nWin32Path = 0,
        wsWin32PathLen = 0;
    int nCoResponse = -1;

    if (szKnownPath == nullptr)
    {
        SetLastError(ERROR_INVALID_PARAMETER);
        return str2GuidReslt;
    }

    if (szKnownPath[0] == L'\0')
    {
        SetLastError(ERROR_INVALID_PARAMETER);
        return str2GuidReslt;
    }

    wsKnownPath = szKnownPath;

    nPos = wsKnownPath.find_first_of(L'\\');

    if (nPos != 0x26u)       // GUID String 长度为 38 字符
    {
        SetLastError(ERROR_PATH_NOT_FOUND);
        return str2GuidReslt;
    }

    wsKnownPath.resize(0x26u);

    SetLastError(0);

    str2GuidReslt = CLSIDFromString((LPCOLESTR)wsKnownPath.c_str(), (LPCLSID)&stGuid);

    if (str2GuidReslt == (HRESULT)NOERROR) {
        //printf("The CLSID was obtained successfully.\n");
        str2GuidReslt = SHGetKnownFolderPath(stGuid, KF_FLAG_DONT_VERIFY, NULL, &strPathPrefix);

        if (SUCCEEDED(str2GuidReslt) && strPathPrefix != nullptr)
        {
            nPrefix = wcslen_s(strPathPrefix, 0x800u);
            nKnownPath = wcslen_s(szKnownPath, 0x800u);

            if (nPrefix == 0 || nKnownPath == 0)
            {
                std::wcerr << L"Get string length faild." << std::endl;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return E_FAIL;
            }

            nWin32Path = nKnownPath - 0x26u + nPrefix + 1;

            // 计算需要分配的缓冲区字节数
            if (SizeTMult(nWin32Path, sizeof(wchar_t), &wsWin32PathLen) != S_OK) {
                // 乘法溢出,处理错误
                std::wcerr << L"Multiplication overflow occurred." << std::endl;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return E_FAIL;
            }

            strWin32Path = (PWSTR)CoTaskMemAlloc(wsWin32PathLen);

            if (strWin32Path == nullptr)
            {
                CoTaskMemFree(strPathPrefix);// 释放内存
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                return E_FAIL;
            }

            nCoResponse = CoCreateSuffixFullPath(strWin32Path,
                nWin32Path, L"%s%s", strPathPrefix, &szKnownPath[0x26]);

            if (nCoResponse && nCoResponse == nWin32Path)
            {
                *szWin32FilePath = strWin32Path;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return NOERROR;
            }

            CoTaskMemFree(strPathPrefix);// 释放内存
            return E_FAIL;
        }

        std::cerr << "SHGetKnownFolderPath Failed,errorCode = "
            << GetLastError() << std::endl;
        return str2GuidReslt;
    }
    std::cerr << "CLSIDFromString Failed,errorCode = "
        << GetLastError() << std::endl;
    return str2GuidReslt;
}

int CoCreateSuffixFullPath(
    wchar_t* wsBuffer,
    size_t wsCount,
    const wchar_t* wsFormat, ...
)
{
    if (wsBuffer == nullptr) return -1;

    int nWriteCount = 0;
    // hold the variable argument 
    va_list argsList = nullptr;

    // A function that invokes va_start 
    // shall also invoke va_end before it returns. 
    va_start(argsList, wsFormat);
    nWriteCount = vswprintf_s(wsBuffer, wsCount, wsFormat, argsList);
    va_end(argsList);

    return ++nWriteCount;
}



// wcslen 安全版本
size_t wcslen_s(
    const wchar_t* str, size_t ccmaxlen)
{
    size_t length = 0;

    if (ccmaxlen > 0x5000)
        ccmaxlen = 0x5000;  // 20480 字节,路径长度应该远小于该值

    __try {
        while (length < ccmaxlen && str[length] != L'\0') {
            ++length;
        }
        // 说明发生越界访问或者超出限制
        if (length == ccmaxlen)
        {
            std::cerr << "Trigger limit: The buffer exceeds the limit of characters." << std::endl;
            return 0;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        // 捕获并处理访问无效指针引发的异常
        std::cerr << "Access violation: Attempted to read from null pointer." << std::endl;
        return 0;
    }

    return length;
}


errno_t __fastcall memset_s(void* v, rsize_t smax, int c, rsize_t n) {
    if (v == NULL) return EINVAL;
    if (smax > RSIZE_MAX) return EINVAL;
    if (n > smax) return EINVAL;
    volatile unsigned char* p = (volatile unsigned char*)v;
    while (smax-- && n--) {
        *p++ = c;
    }
    return 0;
}

当 dwMessage 为 NIM_MODIFY 和 NIM_DELETE 时,过程如下:

dwMessage 为 NIM_MODIFY 和 NIM_DELETE 的处理

NIM_MODIFY 过程检查 uFlags 参数是否包含 NIF_MESSAGE 标志。如果包含则说明 lpData->uCallbackMessage 参数有效,该参数是要发送到 hWnd 窗口的用户自定义消息。此时会检查 lpData->hWnd 是否不为空,如果不为空则会调用 ChangeWindowMessageFilterEx 并指定 MSGFLT_ALLOW 允许 uCallbackMessage 消息通过 UIPI ,取消 UIPI 的限制。UIPI 是 Vista 开始引入的一种消息安全机制,限制低完整级别进程向高完整级别进程发送窗口/线程消息。完成修改后,进入 LABEL_32,再次利用相同函数取消对 TaskbarCreated 字符串命名的消息的限制。随后向下执行 LABEL_24。

如果 dwMessage 为 NIM_DELETE ,则首先检查 uFlags 是否不包含 NIF_MESSAGE 标志,如果不包含,则直接执行 LABEL_24;否则,会检查  lpData->hWnd 是否为空,如果为空,则直接执行 LABEL_24;否则,禁止 uCallbackMessage 消息通过 UIPI,恢复 UIPI 限制。这也很好理解,因为 NIM_DELETE 表明将要删除图标,那么此时 hWnd 肯定不需要再接收消息了。

我们来看一下 LABEL_24,这是整个调用过程中最重要的一个部分。

SendMessageTimeout 函数发送消息

这个部分主要调用了 SendMessageTimeout 函数向最初查找的 Shell_TrayWnd 窗口发送了 0x4a 这个特定的消息,这个消息是什么呢?

通过查阅 MSDN 文档,我们了解到它是 WM_COPYDATA 消息。分析到这里,其实我们已经明显明白 Shell_NotifyIcon 其实只是消息发送接口,而真正的处理在 explorer 的 Shell_TrayWnd 窗口中。但是这一部分也是最难理解的,下面我将对该消息发送过程进行详细的解释。

2.3 分析 WM_COPYDATA 消息

其实早在 2007 年左右,就有工程师逆向分析过资源管理器,在 Geoff Chappell 的个人笔记主页上就有相关的记录。

下文是摘选自

[WM_COPYDATA for Taskbar Interface (geoffchappell.com)]

www.geoffchappell.com/studies/windows/shell/shell32/api/shlnot/copydata.htm?tx=65

的一段文本介绍,我做了一些翻译:

任务栏界面的 WM_COPYDATA 消息

每一个 SHAppBarMessage、Shell_NotifyIcon、 SHEnableServiceObject 和 SHLoadInProc 函数都只不过是一个用于将数据发送到任务栏窗口的接口(在少数情况下用于获取返回数据)。这些数据的打包及其传输方法是(未被文档化的)实现细节,他们的各个功能之间有很多共同点。

[正文部分]

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

ID函数名
0SHAppBarMessage
1Shell_NotifyIcon
2SHEnableServiceObject 或者 SHLoadInProc

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

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

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

.

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

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

0x03B8 

字节 

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

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

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

事实上,现今的 dwData 成员已经不止 0,1,2 三种 ID 了,在我的测试过程中就发现了超过 2 的 ID 号。


看完上面的笔记,大家可能依然处于懵懂的状态,下面我将分享我的分析理解心得:

通过 MSDN 文档并结合 IDA 的伪代码,我们知道 SendMessageTimeoutW 在被 Shell_NotifyIcon 函数调用时候,参数分别为:

参数 1:Shell_TrayWnd 窗口句柄(hWnd);

参数 2:WM_COPYDATA (0x4A) 消息(uMsg);

参数 3:调用方的窗口句柄(wParam);

参数 4:COPYDATASTRUCT 结构体(lParam);

参数 5:指定 SendMessageTimeoutW 发送消息方式的标识符,该值为 0xB,经过查表知该标志位由 SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG 组成;

参数 6:以毫秒间隔计的等待超时时间,参数值为 0x1B58,表示 7 秒;

参数 7:消息返回状态,dwResult 是额外参数,表示消息处理的结果。

为了进一步理解这里 IDA 错误解析的部分参数构造,我们采用 WinDbg 调试分析该函数。

我首先准备了一个测试程序,该程序调用 Shell_NotifyIcon 并显示一个提示信息,代码如下。

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

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

// 定义全局变量  
NOTIFYICONDATAW lpNotifyData = { 0 };

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

int main()
{
    SetConsoleCodePageToUTF8();

    // 初始化 NOTIFYICONDATA 结构
    lpNotifyData.cbSize = (DWORD)sizeof(NOTIFYICONDATAW);
    lpNotifyData.hWnd = GetConsoleWindow();
    lpNotifyData.uID = 0x5;
    lpNotifyData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
    lpNotifyData.uCallbackMessage = WM_IAWENTRAY;
    lpNotifyData.hIcon = LoadIconW(NULL, IDI_APPLICATION);
    wcscpy_s(lpNotifyData.szTip, L"通知栏图标测试程序");
    wcscpy_s(lpNotifyData.szInfo, L"提示窗口内容");
    wcscpy_s(lpNotifyData.szInfoTitle, L"提示窗口标题");
    lpNotifyData.dwInfoFlags = NIIF_INFO;
    lpNotifyData.uTimeout = 5000;
    Shell_NotifyIconW(NIM_ADD, &lpNotifyData);  // 在托盘区添加图标

    getchar();

    Shell_NotifyIconW(NIM_DELETE, &lpNotifyData);  // 删除图标

    return 0;
}

运行时候的效果如图所示:

通知区域的气泡提示消息框

用 WinDbg 调试测试程序并设置 USER32!SendMessageTimeoutW 断点。随后让测试程序(调用 Shell_NotifyIcon 函数)运行起来并触发断点。

我们查看此时的寄存器,以便于获取前四个参数的值:

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 结构体的指针。

根据 MSDN 的文档: [https://learn.microsoft.com/zh-cn/windows/win32/dataxchg/wm-copydata]

WM_COPYDATA 消息发送时,wParam 为传递数据的窗口的句柄,lParam 为指向包含要传递的数据 的 COPYDATASTRUCT 结构的指针。 COPYDATASTRUCT 结构体的声明如下:

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

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

ID函数名
0SHAppBarMessage
1Shell_NotifyIcon
2SHEnableServiceObject 或者 SHLoadInProc

在从 NT 5.2 开始至今的多次改版中,explorer 已经新增了一些该表未能够展示的功能。

比如 ID 为 3 可以代表调用了 Shell_NotifyIconGetRect 函数(Win 7 引入),该函数可以获取通知图标的边框的屏幕坐标(用于通知图标显示自定义菜单时确定窗口的位置)。

SHSTDAPI Shell_NotifyIconGetRect(
    const NOTIFYICONIDENTIFIER *identifier,
    RECT                       *iconLocation
);

第一个是入参,使用仅 guidItem 或者 hWnd 加 uID 来向 explorer 请求数据,该过程中使用一个未文档的 CLSID。他的调试符号是 GUID_ShellNotifyChevron 具体的 GUID 为:{964B6543-BBAD-44EE-848A-3A95D85951EA}。

Shell_NotifyIconGetRect 伪代码

当然,对于本文的目的来说这些新增的内容不太重要。

由于我们测试用的是 Shell_NotifyIcon,则结果应该是 1,WinDbg 查看发现是一致的:

 0:000> dps 000000f4d95ee308

000000f4`d95ee308        00000000`00000001   ---->  dwData
000000f4`d95ee310        00000000`000005cc   ---->  cbData
000000f4`d95ee318        000000f4`d95ee360   ---->  lpData
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 是什么呢?我们接着分析:

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) 和 dwMessage 合并到这个结构体中第一个成员的前面。explorer 通过Signature 验证调用是否是发送至任务栏的通知区域的,因为 lParam->dwData 指向的 ID == 1 并不一定是由 Shell_NotifyIcon 发起的,只是说 Shell_NotifyIcon 发起时的 ID 为 1。

此外,可能是因为版本问题,HICON hIcon 成员对应位置被改成 DWORD uIconID (UnKnown)。这个值在测试过程中是 01002b,他是 BitMap 结构化图标信息的访问句柄。

根据 IDA 反汇编的代码,ICON 是创建之后才单独去绘制的,所以可能结构体有所变化,其他位置目前没有发现变化,完整的结构体如下所示。

typedef struct _TRAY_ICON_DATAW {
    DWORD Signature;
    DWORD dwMessage;   // dwMessage <-- Shell_NotifyIconW(DWORD dwMessage, PNOTIFYICONDATAW lpData)
    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;

2.4 调用方挂钩拦截消息封送过程

为了验证我们分析的结论,下面我将通过调用方挂钩拦截这个消息的封送过程。

我们利用 detours 库挂钩 SendMessageTimeoutW 函数,并对 TRAY_ICON_DATAW 进行解析,就可以获取发送的消息内容。

一个测试代码如下:

#include <stdio.h>
#include <windows.h>
#include <tchar.h>  
#include <shellapi.h>
#include <string.h>
#include <detours.h>
#include <locale.h>

#pragma comment(lib, "detours.lib")
#define WM_IAWENTRAY  WM_USER + 0x5  // 系统托盘的自定义消息

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

// 托盘信息结构体
typedef struct _TRAY_ICON_DATAW {
    DWORD Signature;
    DWORD dwMessage;   // dwMessage <-- Shell_NotifyIconW(DWORD dwMessage, PNOTIFYICONDATAW lpData)
    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;


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

// Utf-8 为了兼容中文
void SetConsoleCodePageToUTF8() {
    setlocale(LC_ALL, "zh-CN");
    //SetConsoleOutputCP(CP_UTF8);
}

// 定义挂钩函数
BOOL WINAPI MySendMessageTimeoutW(
    HWND hWnd,
    UINT Msg,
    WPARAM wParam,
    LPARAM lParam,
    UINT fuFlags,
    UINT uTimeout,
    PDWORD_PTR lpdwResult
)
{
    // 显示调用参数
    printf("SendMessageTimeoutW called: \n");
    printf("hWnd: 0x%0I64X\nMsg: %u\nwParam: 0x%0I64X\nlParam: 0x%0I64X\n", 
        (LONGLONG)hWnd, Msg, wParam, lParam);

    // 如果 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
        printf("pData: 0x%0I64X", (UINT64)lpIconData);
        printf("hWnd: 0x%04X\n", lpIconData->hWnd);
        printf("szTip: %ws\n", lpIconData->szTip);
        printf("szInfo: %ws\n", lpIconData->szInfo);
        printf("szInfoTitle: %ws\n", lpIconData->szInfoTitle);
    }
    // 调用原始函数
    BOOL result = ((RealSendMessageTimeoutW)Real_SendMessageTimeoutW)(
        hWnd, Msg, wParam, lParam, fuFlags, uTimeout, lpdwResult);
    return result;
}

int main()
{
    SetConsoleCodePageToUTF8();

    // 挂钩 SendMessageTimeoutW
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    Real_SendMessageTimeoutW = &SendMessageTimeoutW;
    DetourAttach(&(PVOID&)Real_SendMessageTimeoutW, MySendMessageTimeoutW);
    DetourTransactionCommit();

	// 初始化NOTIFYICONDATA结构
    lpNotifyData.cbSize = (DWORD)sizeof(NOTIFYICONDATAW);
    lpNotifyData.hWnd = GetConsoleWindow();
    lpNotifyData.uID = 0x5;
    lpNotifyData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
    lpNotifyData.uCallbackMessage = WM_IAWENTRAY;
    lpNotifyData.hIcon = LoadIconW(NULL, IDI_APPLICATION);
    wcscpy_s(lpNotifyData.szTip, L"通知栏图标测试程序");
    wcscpy_s(lpNotifyData.szInfo, L"提示窗口内容");
    wcscpy_s(lpNotifyData.szInfoTitle, L"提示窗口标题");
    lpNotifyData.dwInfoFlags = NIIF_INFO;
    lpNotifyData.uTimeout = 5000;
    Shell_NotifyIconW(NIM_ADD, &lpNotifyData);  // 在托盘区添加图标

	getchar();

	Shell_NotifyIconW(NIM_DELETE, &lpNotifyData);  // 删除图标

    // 卸载挂钩
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&(PVOID&)Real_SendMessageTimeoutW, MySendMessageTimeoutW);
    DetourTransactionCommit();

    getchar();
	return 0;
}

可以很轻松地解析出发送的数据内容,结果如下图所示:

Win11调用方挂钩效果

在 Win 7 的效果如下:

Win7调用方挂钩效果

下面我们解释一下这几个参数,szInfo 和 szInfoTitle 只有在显示气泡提示时候才用到,分别对应消息的内容和标题,如下图所示:

Win11 气泡提示的标题取决于程序名称

szTip 则是任务栏图标的标签文本:

通知区域图标

这说明了对 WM_COPYDATA 消息的分析是正确的。

2.5 接收方挂钩实现获取任意图标信息

讲完了消息封送过程(调用方)的拦截方法,我们必然需要去研究接收方的消息处理机制。这是因为接收方作为消息的总管理者,我们有能力获取所有使用 Shell_NotifyIcon 函数的程序而不必大动干戈地去想着全局挂钩消息的封送过程。全局挂钩往往消耗过多的计算机资源且面临着兼容性风险从而不是一个优选的方案。

根据我前一阵子研究的动态壁纸制作技术内幕,当时就已经分析了一部分 explorer 内部的桌面机制,分析时遇到了一类消息回调的模板,里面就有这里需要用的内容,只不过第二部分的动态壁纸制作分析文章搁置了很久而未发布。原因是这真的是一个很复杂的工程,而我又没有太多空余时间,所以进展就比较慢,下面是部分未发布的内容截图:

部分未发布的内容截图

当然,关于桌面动态壁纸和虚拟桌面等接口,未来有进展了我就会先发布一部分的。

通过 IDA 分析 explorer 的导入表,我们对所有窗口创建和消息发送函数进行交叉引用、特征字符串等排查。最终确定了 CTray 类是实现系统通知区域的窗口程序类,并在其中找到了 CTray::v_WndProc 消息回调函数,在 explorer 所有的窗口回调函数中只有该函数处理 WM_COPYDATA(0x4A) 消息。

符号名称:?v_WndProc@CTray@@MEAA_JPEAUHWND__@@I_K_J@Z

解析名称:CTray::v_WndProc

随后我们结合 WinDbg 调试进一步确定了这个函数就是处理 WM_COPYDATA 的函数,由于 explorer 不是很好动态调试,所以测试时没有留图。

在 x64 系统下,该函数的声明(已经分析并修正参数)如下:

typedef LRESULT(__fastcall* v_WndProc)(
    LPVOID pthis,   // CTray *this,由于没有 CTray 的原型,这里就改写成 void* 类型。
    HWND hWnd, 
    UINT uMsg, 
    WPARAM wParam, 
    LPARAM lParam
);

我猜测 x86 的调用约定是 __stdcall ,具体声明如何还未进行验证,交给读者啦。

它后面几个参数其实和一般的窗口回调没有什么区别。

最后两个参数 IDA 很有可能根据调试符号直接解析为 LPITEMIDLIST 结构体指针,我觉得这是不对的,就改了过来。注意: IDA 对最后一个参数名称解析为 pidl 。

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

由于这里具体的处理过程理解起来比较复杂,而且我觉得暂时也没有必要去讲解。所以我就不逐一解释了,感兴趣的可以自己去分析一下这个函数,它相对于桌面其他几个窗口的消息处理机制来说还是稍微简单一点的。

下面的代码使用 Detours 库实现挂钩过程,并使用了 CTray::v_WndProc 偏移量的硬编码,实际使用时应该采用特征码定位方法。定位过程用到的代码附加在后面,需要的自己完善一下代码。

我测试的系统是最新的 Win 11 Release 23H2 | x64 | Build 10.0.22631.3155,该函数偏移量为 0xB630。

2024/03/10: 03 累积更新 22631.3296 版本补丁了 explorer.exe,现在该函数的偏移量为 0xB680

2024/09/28: 10.0.22631.4169 模块中该函数地址:0x7ff7bd8d0380,偏移量为:0x10380。运行特征码定位或 ExtraBytesHacker 代码获取地址信息。

P.S.:

(1)可以不使用挂钩 CTray::v_WndProc 函数的方式拦截 WM_COPYDATA 消息,详细见第 4 篇文章《最新方案(三)》

(2)Explorer 属于经常更新的组件,且 CTray::v_WndProc 函数是内部函数,其偏移量可能随系统更新而变化。如果使用定位算法来动态获取地址,建议选取稳定的特征码或者解析调试符号

下面是 HOOK 模块的简单测试用代码(使用了硬编码!!!):

#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;      // CTray::v_WndProc 函数原始地址

// 一些宏定义
#define NIM_ADD         0x00000000
#define NIM_MODIFY      0x00000001
#define NIM_DELETE      0x00000002
#define NIM_SETFOCUS    0x00000003
#define NIM_SETVERSION  0x00000004

#define NIF_TIP         0x00000004
#define NIF_INFO        0x00000010

// Notify Icon Infotip flags
#define NIIF_NONE       0x00000000
#define NIIF_INFO       0x00000001
#define NIIF_WARNING    0x00000002
#define NIIF_ERROR      0x00000003
#define NIIF_USER       0x00000004
#define NIIF_ICON_MASK  0x0000000F
#define NIIF_NOSOUND    0x00000010
#define NIIF_LARGE_ICON 0x00000020
#define NIIF_RESPECT_QUIET_TIME 0x00000080

// 窗口句柄转换字符串的函数
std::wstring make_hwnd_text(HWND hwnd)
{
    wchar_t buf[25];
    wsprintfW(buf, L"HWND:0x%I64X", (UINT64)hwnd);
    return buf;
}

// dwMessage 参数转换为已知参数字符串
std::wstring make_snmsg_text(DWORD dwMessage)
{
#define CHECK_DMSG(dwMessage, var) if (dwMessage == var) return L#var;
    CHECK_DMSG(dwMessage, NIM_ADD);
    CHECK_DMSG(dwMessage, NIM_MODIFY);
    CHECK_DMSG(dwMessage, NIM_DELETE);
    CHECK_DMSG(dwMessage, NIM_SETFOCUS);
    CHECK_DMSG(dwMessage, NIM_SETVERSION);

    wchar_t buf[25];
    wsprintfW(buf, L"Message:%u", dwMessage);
    return buf;
#undef CHECK_HWND
}

// dwInfoFlags 参数转换为已知参数字符串
std::wstring make_infoflag_text(DWORD dwInfoFlags)
{
#define CHECK_DMSG(dwInfoFlags, var) if (dwInfoFlags == var) return L#var;
    CHECK_DMSG(dwInfoFlags, NIIF_NONE);
    CHECK_DMSG(dwInfoFlags, NIIF_INFO);
    CHECK_DMSG(dwInfoFlags, NIIF_WARNING);
    CHECK_DMSG(dwInfoFlags, NIIF_ERROR);
    CHECK_DMSG(dwInfoFlags, NIIF_USER);
    CHECK_DMSG(dwInfoFlags, NIIF_ICON_MASK);
    CHECK_DMSG(dwInfoFlags, NIIF_NOSOUND);
    CHECK_DMSG(dwInfoFlags, NIIF_LARGE_ICON);
    CHECK_DMSG(dwInfoFlags, NIIF_RESPECT_QUIET_TIME);
    wchar_t buf[25];
    wsprintfW(buf, L"Message:%u", dwInfoFlags);
    return buf;
#undef CHECK_HWND
}

// 方便于调用
#define HWND2TEXT(hwnd) make_hwnd_text(hwnd).c_str()
#define DMSG2TEXT(dwMessage) make_snmsg_text(dwMessage).c_str()
#define NIIF2TEXT(dwInfoFlags) make_infoflag_text(dwInfoFlags).c_str()

// 日志记录函数,在模块的目录下创建 SysNotifyLog.txt 日志文件
void log_printf(const wchar_t* fmt, ...)
{
    DWORD dwError = GetLastError();
    setlocale(LC_ALL, "zh-CN");  // 设置代码页以支持中文
    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> ",
                GetCurrentProcessId(), GetCurrentThreadId());
            fputws(buf, fp);
            fflush(fp);
            fclose(fp);
        }
    }
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);

    SetLastError(dwError);
}

// 结构体的声明
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;

// 函数原型的声明
typedef LRESULT(__fastcall* __v_WndProc)(
    LPVOID pthis, 
    HWND hWnd, 
    UINT uMsg, 
    WPARAM wParam, 
    LPARAM lParam
);


// 我们的钩子函数
LRESULT __fastcall v_WndProc(
    LPVOID pthis,   HWND hWnd, 
    UINT   uMsg,    WPARAM wParam, 
    LPARAM lParam
)
{
    LRESULT ret = 0;
    ret = ((__v_WndProc)fpVWndProc)(pthis, hWnd, uMsg, wParam, lParam);
    
    if ((UINT)uMsg == WM_COPYDATA)
    {
        COPYDATASTRUCT* pCopyData = reinterpret_cast<COPYDATASTRUCT*>(lParam);
        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:[%ws]:[%ws];\n",
                    DMSG2TEXT(pTrayIcon->dwMessage), HWND2TEXT((HWND)wParam));
                if ((pTrayIcon->uFlags & NIF_INFO) != 0)
                {
                    log_printf(L"Tip[%ws], szInfoParam:\n",
                        pTrayIcon->szTip);
                    log_printf(L"InfoTitle[%ws], Info[%ws], InfoFlags[%ws];\n",
                        pTrayIcon->szInfoTitle, pTrayIcon->szInfo, NIIF2TEXT(pTrayIcon->dwInfoFlags));
                }
                else if ((pTrayIcon->uFlags & NIF_TIP) != 0)
                {
                    log_printf(L"Tip[%ws], non-szInfo;\n", pTrayIcon->szTip);
                }
                else {
                    log_printf(L"non-szTip, non-szInfo;\n");
                }
                
                // log_printf(L"CTray::v_WndProc: leave: ret = %Id;\n", ret);
            }
            
        }
    }
    return ret;
}


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

    HMODULE hExpBase = GetModuleHandleW(L"explorer.exe");
    if (hExpBase == nullptr)
        return;

#if (NTDDI_VERSION >= NTDDI_WIN10 && _WIN64)
    // 硬编码了 v_WndProc 函数的偏移量,系统版本 Win11, 10.0.22631.3155, x64, 23H2
    fpVWndProc = reinterpret_cast<LPVOID>(
        reinterpret_cast<UINT64>(hExpBase) + 0xB630u);
#else
    NOT_SUPPORT_VERSION;
#endif

    if (fpVWndProc == nullptr)
        return;

    // 将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。
    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();
        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
}

日志记录的效果如下图所示:

测试日志记录截图[旧版]

P.S.:有趣的是在寻找是否已经存在解决方案的时候,我偶然找到了 ReactOS 团队于 2017 年上半年公开的 explorer.exe 消息挂钩工具的代码。虽然在最新的系统上已经不再适用,并且它主要解析 explorer 作为消息发送者的情况,而不是接收者,但是他们的模板对我来说还是有所帮助的。其 Github 链接如下:SysNotifyHooker: API hook for Windows Explorer

提示:根据最新的研究(《最新方案(三)》),获取 CTray::v_WndProc 函数地址不需要特征码定位,可以利用 GetWindowLongPtr 函数获取,代码如下:

// ExtraBytesHacker 代码
// 此代码用于获取 CTray::v_WndProc 函数的动态链接地址
// 作者:LianYou516
// 时间:2024.09.28
// 
#include <Windows.h>
#include <iostream>

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

typedef struct _ctray_vtable {
	ULONG_PTR vTable;
	ULONG_PTR AddRef;
	ULONG_PTR Release;
	ULONG_PTR WndProc;
} CTray;

int main()
{
	CTray     ct{};
	ULONG_PTR ctp;
	HWND      hw;
	HANDLE    hp;
	DWORD     pid;
	SIZE_T    wr;
	hw = FindWindowA("Shell_TrayWnd", NULL);
	GetWindowThreadProcessId(hw, &pid);
	hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	ctp = GetWindowLongPtrW(hw, NULL);
	ReadProcessMemory(hp, (LPVOID)ctp, (LPVOID)&ct.vTable, 
		sizeof(ULONG_PTR), &wr);
	ReadProcessMemory(hp, (LPVOID)ct.vTable, (LPVOID)&ct.AddRef, 
		sizeof(ULONG_PTR) * 3, &wr);
	std::cout << "ct.WndProc: 0x" << std::hex << ct.WndProc << std::endl;
	CloseHandle(hp);
	return 0;
}

所以,模块代码可以改为:

#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")
#pragma comment(lib, "user32.lib")

HINSTANCE g_hinstDLL = NULL;   // 模块句柄
LPVOID fpVWndProc = NULL;      // CTray::v_WndProc 函数原始地址

// 一些宏定义
#define NIM_ADD         0x00000000
#define NIM_MODIFY      0x00000001
#define NIM_DELETE      0x00000002
#define NIM_SETFOCUS    0x00000003
#define NIM_SETVERSION  0x00000004

#define NIF_TIP         0x00000004
#define NIF_INFO        0x00000010

// Notify Icon Infotip flags
#define NIIF_NONE       0x00000000
#define NIIF_INFO       0x00000001
#define NIIF_WARNING    0x00000002
#define NIIF_ERROR      0x00000003
#define NIIF_USER       0x00000004
#define NIIF_ICON_MASK  0x0000000F
#define NIIF_NOSOUND    0x00000010
#define NIIF_LARGE_ICON 0x00000020
#define NIIF_RESPECT_QUIET_TIME 0x00000080

// 窗口句柄转换字符串的函数
std::wstring make_hwnd_text(HWND hwnd)
{
    wchar_t buf[25];
    wsprintfW(buf, L"HWND:0x%I64X", (UINT64)hwnd);
    return buf;
}

// dwMessage 参数转换为已知参数字符串
std::wstring make_snmsg_text(DWORD dwMessage)
{
#define CHECK_DMSG(dwMessage, var) if (dwMessage == var) return L#var;
    CHECK_DMSG(dwMessage, NIM_ADD);
    CHECK_DMSG(dwMessage, NIM_MODIFY);
    CHECK_DMSG(dwMessage, NIM_DELETE);
    CHECK_DMSG(dwMessage, NIM_SETFOCUS);
    CHECK_DMSG(dwMessage, NIM_SETVERSION);

    wchar_t buf[25];
    wsprintfW(buf, L"Message:%u", dwMessage);
    return buf;
#undef CHECK_HWND
}

// dwInfoFlags 参数转换为已知参数字符串
std::wstring make_infoflag_text(DWORD dwInfoFlags)
{
#define CHECK_DMSG(dwInfoFlags, var) if (dwInfoFlags == var) return L#var;
    CHECK_DMSG(dwInfoFlags, NIIF_NONE);
    CHECK_DMSG(dwInfoFlags, NIIF_INFO);
    CHECK_DMSG(dwInfoFlags, NIIF_WARNING);
    CHECK_DMSG(dwInfoFlags, NIIF_ERROR);
    CHECK_DMSG(dwInfoFlags, NIIF_USER);
    CHECK_DMSG(dwInfoFlags, NIIF_ICON_MASK);
    CHECK_DMSG(dwInfoFlags, NIIF_NOSOUND);
    CHECK_DMSG(dwInfoFlags, NIIF_LARGE_ICON);
    CHECK_DMSG(dwInfoFlags, NIIF_RESPECT_QUIET_TIME);
    wchar_t buf[25];
    wsprintfW(buf, L"Message:%u", dwInfoFlags);
    return buf;
#undef CHECK_HWND
}

// 方便于调用
#define HWND2TEXT(hwnd) make_hwnd_text(hwnd).c_str()
#define DMSG2TEXT(dwMessage) make_snmsg_text(dwMessage).c_str()
#define NIIF2TEXT(dwInfoFlags) make_infoflag_text(dwInfoFlags).c_str()

// 日志记录函数,在模块的目录下创建 SysNotifyLog.txt 日志文件
void log_printf(const wchar_t* fmt, ...)
{
    DWORD dwError = GetLastError();
    setlocale(LC_ALL, "zh-CN");  // 设置代码页以支持中文
    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> ",
                GetCurrentProcessId(), GetCurrentThreadId());
            fputws(buf, fp);
            fflush(fp);
            fclose(fp);
        }
    }
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);

    SetLastError(dwError);
}

// 结构体的声明

typedef struct _ctray_vtable {
	ULONG_PTR vTable;
	ULONG_PTR AddRef;
	ULONG_PTR Release;
	ULONG_PTR WndProc;
} CTray;


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;

// 函数原型的声明
typedef LRESULT(__fastcall* __v_WndProc)(
    LPVOID pthis, 
    HWND hWnd, 
    UINT uMsg, 
    WPARAM wParam, 
    LPARAM lParam
);


// 我们的钩子函数
LRESULT __fastcall v_WndProc(
    LPVOID pthis,   HWND hWnd, 
    UINT   uMsg,    WPARAM wParam, 
    LPARAM lParam
)
{
    LRESULT ret = 0;
    ret = ((__v_WndProc)fpVWndProc)(pthis, hWnd, uMsg, wParam, lParam);
    
    if ((UINT)uMsg == WM_COPYDATA)
    {
        COPYDATASTRUCT* pCopyData = reinterpret_cast<COPYDATASTRUCT*>(lParam);
        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:[%ws]:[%ws];\n",
                    DMSG2TEXT(pTrayIcon->dwMessage), HWND2TEXT((HWND)wParam));
                if ((pTrayIcon->uFlags & NIF_INFO) != 0)
                {
                    log_printf(L"Tip[%ws], szInfoParam:\n",
                        pTrayIcon->szTip);
                    log_printf(L"InfoTitle[%ws], Info[%ws], InfoFlags[%ws];\n",
                        pTrayIcon->szInfoTitle, pTrayIcon->szInfo, NIIF2TEXT(pTrayIcon->dwInfoFlags));
                }
                else if ((pTrayIcon->uFlags & NIF_TIP) != 0)
                {
                    log_printf(L"Tip[%ws], non-szInfo;\n", pTrayIcon->szTip);
                }
                else {
                    log_printf(L"non-szTip, non-szInfo;\n");
                }
                
                // log_printf(L"CTray::v_WndProc: leave: ret = %Id;\n", ret);
            }
            
        }
    }
    return ret;
}


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

    HMODULE hExpBase = GetModuleHandleW(L"explorer.exe");
    if (hExpBase == nullptr)
        return;

#if (NTDDI_VERSION >= NTDDI_WIN10 && _WIN64)
    CTray     ct{};
	ULONG_PTR ctp;
	HWND      hw;
	HANDLE    hp;
	DWORD     pid;
	SIZE_T    wr;
	hw = FindWindowA("Shell_TrayWnd", NULL);
	GetWindowThreadProcessId(hw, &pid);
	hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	ctp = GetWindowLongPtrW(hw, NULL);
	ReadProcessMemory(hp, (LPVOID)ctp, (LPVOID)&ct.vTable,
		sizeof(ULONG_PTR), &wr);
	ReadProcessMemory(hp, (LPVOID)ct.vTable, (LPVOID)&ct.AddRef,
		sizeof(ULONG_PTR) * 3, &wr);
    // v_WndProc 函数的地址
    fpVWndProc = reinterpret_cast<LPVOID>(ct.WndProc);
#else
    #error This module must be compiled as 64-bit.
#endif

    if (fpVWndProc == nullptr)
        return;

    // 将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。
    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();
        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
}

具体说明可以见《最新方案(三)》中 9.1 小节。

特征码定位算法整理可以见我的这篇文章:

https://blog.csdn.net/qq_59075481/article/details/135752520

我在测试时使用的是暴力方法(不是最优的):

// 注:2024.04.13 修正计算时可能存在的越界问题

#include <stdio.h>
#include <windows.h>
#include <vector>
#include <Psapi.h>
#include <time.h>

inline int BFTracePatternInModule(
    LPCWSTR moduleName,
    PBYTE pattern,
    SIZE_T patternSize,
    DWORD dwRepeat,
    DWORD dwSelect = 1
)
{
    if (pattern == 0 || moduleName == 0 || patternSize == 0 || dwRepeat <= 0)
    {
        return 0;
    }

    HMODULE hModule = LoadLibraryW(moduleName);
    if (hModule == nullptr) {
        printf("Failed to load module: %ws.\n", moduleName);
        return 0;
    }

    MODULEINFO moduleInfo;
    if (!GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo))) {
        printf("Failed to get module information.\n");
        FreeLibrary(hModule);
        return 0;
    }

    std::vector<uint64_t> vcMachList;
    BYTE* moduleBase = reinterpret_cast<BYTE*>(hModule);
    SIZE_T moduleSize = moduleInfo.SizeOfImage;

    printf("模块基址:0x%I64X.\n", reinterpret_cast<uint64_t>(hModule));
    printf("模块大小:%I64d Bytes.\n", moduleSize);


    if (moduleSize == 0)
    {
        printf("Failed to get module information.\n");
        FreeLibrary(hModule);
        return 0;
    }

    uint64_t thisMatch = 0;
    DWORD SelectCase = (dwSelect < 256) && dwSelect ? dwSelect : 256; // 最大结果记录次数
    SIZE_T MatchLimit = patternSize * dwRepeat - 1;  // 连续重复匹配次数限制
    int cwStart = clock();

    if (dwRepeat == 1)
    {
        for (SIZE_T i = 0; i < moduleSize - patternSize; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < patternSize - 1; j++)
            {
                if (moduleBase[i + j] != pattern[j] && pattern[j] != 0u)
                {
                    break;
                }
            }

            if (j == patternSize - 1)
            {
                if (moduleBase[i + j] == pattern[j] || pattern[j] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if (!SelectCase) break;
                }
            }
        }
    }
    else {
        for (SIZE_T i = 0; i < moduleSize - MatchLimit - 1; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < MatchLimit; j++)
            {
                if (moduleBase[i + j] != pattern[j % patternSize] && pattern[j % patternSize] != 0u)
                {
                    break;
                }
            }

            if (j == MatchLimit)
            {
                if (moduleBase[i + MatchLimit] == pattern[patternSize - 1] || pattern[patternSize - 1] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if (!SelectCase) break;
                }
            }
        }
    }

    int cwEnd = clock();

    for (SIZE_T i = 0; i < vcMachList.size(); i++)
    {
        printf("匹配到模式字符串位于偏移: [0x%I64X] 处,动态地址:[0x%I64X]。\n",
            vcMachList[i], reinterpret_cast<uint64_t>(moduleBase) + vcMachList[i]);
    }

    if (vcMachList.size() == 0)
    {
        printf("No Found.\n");
    }

    FreeLibrary(hModule);
    return cwEnd - cwStart;
}

/*
*       CTray::v_WndProc 函数入口点汇编
*       .text:000000014000B630 48 89 5C 24 08                mov     [rsp-8+arg_0], rbx
*       .text:000000014000B635 48 89 74 24 10                mov     [rsp-8+arg_8], rsi
*       .text:000000014000B63A 55                            push    rbp
*       .text:000000014000B63B 57                            push    rdi
*       .text:000000014000B63C 41 54                         push    r12
*       .text:000000014000B63E 41 56                         push    r14
*       .text:000000014000B640 41 57                         push    r15
*       .text:000000014000B642 48 8D 6C 24 D1                lea     rbp, [rsp-2Fh]
*       .text:000000014000B647 48 81 EC E0 00 00 00          sub     rsp, 0E0h
*/

int main()
{
    // 暴力算法
    const wchar_t* moduleName = L"C:\\Windows\\explorer.exe";
    // 测试使用的是入口点特征码,实际需要自己调整选择最佳的特征
    BYTE   pattern[] =
    {
        0x48u, 0x89u, 0x5Cu, 0x24u, 0x08u,
        0x48u, 0x89u, 0x74u, 0x24u, 0x10u,
        0x55u, 0x57u, 0x41u, 0x54u, 0x41u,
        0x56u, 0x41u, 0x57u
    };
    SIZE_T patternSize = 18;
    DWORD dwRepeat = 1, dwSelect = 1; // 匹配第一次完整匹配,不重复匹配
    int TimeCost = 0;
    TimeCost = BFTracePatternInModule(moduleName,
        pattern, patternSize, dwRepeat, dwSelect);
    printf("算法耗时:%d ms.\n", TimeCost);
    return 0;
}

测试结果截图如下:

特征码定位算法计算结果

3 注入 HOOK 模块的方案

对于上面 2.5 节的 Dll 代码,需要重启资源管理器并且在其初始化的特定阶段进行模块注入。一方面注入的过早 explorer 的环境还没有初始化完成,这时候注入是有风险的——启动挂起进程注入有时候会失败;另一方面,注入的过晚 Shell_TrayWnd 窗口已经创建,这时候再处理就迟了,只能拦截到后续注册的图标信息。

我尝试了在稍晚一些的阶段进行注入,比如 ntdll、Kernel32、KernelBase 等重要的模块已经加载完成时,注入依然有概率失败。但是创建挂起进程注入作为经典的注入方式,不能不去谈。

3.1 创建挂起进程注入

一般在进程创建时注入模块,需要使用创建挂起进程注入。通过 CreateProcess 函数并指定 dwCreationFlags 包含 CREATE_SUSPENDED ,即可创建挂起进程。

再使用 NtCreateThreadEx 函数创建远程线程。对于挂起进程的 explorer 也比较特殊,需要指定 ULONG CreateThreadFlags 参数为 1 来创建挂起的远程线程线程。

随后在释放线程的时候,必须先释放主线程,再释放我们的远程线程。否则,进程就会崩溃。

std::cout << "> Resume threads: \n" << std::endl;

// 先释放主线程
ResumeThread(hThread);

// 再释放远程线程
if(ResumeThread(hRemoteThread) == (DWORD)-1)
{
    std::cout << "Error: Resume threads failed! \n" << std::endl;
    return FALSE;
}

完整的注入器代码如下:

#include <windows.h>
#include <iostream>
#include <restartmanager.h>
#include <TlHelp32.h>
#include <vector>

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

#define RM_SESSIONKEY_LEN sizeof(GUID) * 2 + 1

BOOL WINAPI InjectMouldeHandler(
    HANDLE hProcess,
    HANDLE hThread,
    LPCWSTR pszDllFileName
);

BOOL GetShellProcessRmInfoEx(
    PRM_UNIQUE_PROCESS* lpRmProcList,
    DWORD_PTR* lpdwCountNum
);
void RmProcMemFree(
    PRM_UNIQUE_PROCESS lpRmProcList,
    DWORD_PTR lpdwCountNum
);

void RmWriteStatusCallback(
    UINT nPercentComplete
);

DWORD ShellShutdownManager();

int wmain(int argc, WCHAR* argv[])
{
    WCHAR szAppName[256] = L"C:\\Windows\\explorer.exe";
    //WCHAR szDllName[256] = { 0 };
    BOOL cpResult = FALSE,
        isInjected = FALSE;
    STARTUPINFO si = { 0 };
    si.cb = sizeof(STARTUPINFO);
    PROCESS_INFORMATION pi = { 0 };

    // 参数检查
    if (argc != 2)
    {
        std::cerr << "Error invalid parameters.\n" << 
            " <Usage> SuspendResumeInjectShell.exe <Payload Path>"
            << std::endl;
        return -1;
    }


    // 使用 RM 结束 SHELL 进程
    std::cout << "> Shutdown Shell process " << std::endl;
    if (ERROR_SUCCESS != ShellShutdownManager())
    {
        std::cerr << "Shutdown Shell process failed.\n" << std::endl;
        return -1;
    }

    // 启动挂起式进程
    std::cout << "> Create suspend process " << std::endl;
    cpResult = CreateProcessW( NULL, szAppName,
        nullptr, nullptr, FALSE,
        CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
        nullptr, nullptr, &si, &pi );

    std::cout << "\thProcess: 0x"
        << std::hex << (UINT64)pi.hProcess
        << "\thThread: 0x"
        << std::hex << (UINT64)pi.hThread
        << "\tPID: "
        << std::dec << pi.dwProcessId
        << "\tTID: "
        << std::dec << pi.dwThreadId
        << std::endl;


    // 远程线程注入
    std::cout << "> Inject Moulde to process: \n" << std::endl;
    isInjected = InjectMouldeHandler(pi.hProcess, pi.hThread, argv[1]);

    if (isInjected == TRUE)
    {
        std::cout << "Inject Successfully!" << std::endl;
    }

    // 关闭句柄
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    return 0;
}


BOOL WINAPI InjectMouldeHandler(
    HANDLE hProcess,
    HANDLE hThread,
    LPCWSTR pszDllFileName
)
{
    // 1.目标进程句柄
    if ((hProcess == nullptr) || (hThread == nullptr)
        || (pszDllFileName == nullptr))
    {
        wprintf(L"Error: InvalidSyntax error from InjectMouldeHandler.\n");
        SetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }

    size_t pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);

    SetLastError(0);
    // 2.在目标进程中申请空间
    LPVOID lpPathAddr = VirtualAllocEx(hProcess, 0, pathSize,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    if (lpPathAddr == nullptr)
    {
        wprintf(L"Error[%d]: Failed to apply memory in the target process!\n", GetLastError());
        return FALSE;
    }

    SetLastError(0);
    // 3.在目标进程中写入 Dll 路径
    if (FALSE == WriteProcessMemory(hProcess, lpPathAddr,
        pszDllFileName, pathSize, NULL))
    {
        wprintf(L"Error[%d]: Failed to write module path in target process!\n", GetLastError());
        return FALSE;
    }

    SetLastError(0);
    // 4.加载 ntdll.dll
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");

    if (hNtdll == NULL)
    {
        wprintf(L"Error[%d]: Failed to load NTDLL.DLL!\n", GetLastError());
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    SetLastError(0);
    // 5.获取 LoadLibraryW 的函数地址, FARPROC 可以自适应 32 位与 64 位
    FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandleW(L"KernelBase.dll"),
        "LoadLibraryW");

    if (pFuncProcAddr == nullptr)
    {
        wprintf(L"Error[%d]: Failed to obtain the address of the LoadLibrary function!\n",
            GetLastError());
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    // 6.获取 NtCreateThreadEx 函数地址,该函数在32位与64位下原型不同
    // _WIN64 用来判断编译环境 ,_WIN32用来判断是否是 Windows 系统
#ifdef _WIN64
    typedef DWORD(WINAPI* __NtCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown
        );
#else
    typedef DWORD(WINAPI* __NtCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown
        );
#endif

    SetLastError(0);
    __NtCreateThreadEx NtCreateThreadEx =
        (__NtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");

    if (NtCreateThreadEx == nullptr)
    {
        wprintf(L"Error[%d]: Failed to obtain NtCreateThreadEx function address!\n",
            GetLastError());
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    SetLastError(0);

    // 7.在目标进程中创建远线程
    HANDLE hRemoteThread = nullptr;
    DWORD lpExitCode = 0;
    DWORD dwStatus = NtCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
        hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr,
        1,  // 挂起创建线程
        0, 0, 0, NULL);

    if (hRemoteThread == nullptr)
    {
        wprintf(L"Error[%d]: Failed to create thread in target process!\n", GetLastError());
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    std::cout << "> Resume threads: \n" << std::endl;

    // 先释放主线程
    ResumeThread(hThread);

    // 再释放远程线程
    if(ResumeThread(hRemoteThread) == (DWORD)-1)
    {
        std::cout << "Error: Resume threads failed! \n" << std::endl;
        return FALSE;
    }

    SetLastError(0);
    // 8.等待线程结束
    if (WAIT_TIMEOUT == WaitForSingleObject(hRemoteThread, 7000))
    {
        wprintf(L"Error[%d]: Remote thread not responding.\n", GetLastError());
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    GetExitCodeThread(hRemoteThread, &lpExitCode);  // 返回值应该是 LoadLibrary 返回的程序注入的地址?
    
    if (lpExitCode == 0)
    {
        wprintf(L"Error: Injection module failed in the target process.\n");
        VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    // 9.清理环境
    VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
    CloseHandle(hRemoteThread);
    return TRUE;
}


// 控制结束 explorer 进程
DWORD ShellShutdownManager()
{
    DWORD dwRmStatus = 0;
    DWORD dwSessionHandle = 0;
    WCHAR strSessionKey[RM_SESSIONKEY_LEN] = { 0 };
    PRM_UNIQUE_PROCESS lpRmProcList = nullptr;
    DWORD_PTR lpdwCountNum = 0u;

    // 启动重启管理器会话
    dwRmStatus = RmStartSession(&dwSessionHandle, NULL, strSessionKey);
    if (ERROR_SUCCESS != dwRmStatus)
    {
        std::cerr << "RmStartSession failed: " << std::dec << dwRmStatus << std::endl;
        return DWORD(-1);
    }
    // 获取 explorer 进程信息
    if (!GetShellProcessRmInfoEx(&lpRmProcList, &lpdwCountNum))
    {
        std::cerr << "GetShellProcessRmInfoEx failed." << std::endl;
        RmEndSession(dwSessionHandle);
        return DWORD(-1);
    }

    // 进程数
    UINT dwNum = static_cast<UINT>(lpdwCountNum);

    if (dwNum == UINT(0))  // 没有找到进程
    {
        std::cerr << "There are no shell processes that need to be closed." << std::endl;
        return dwSessionHandle;
    }

    // GetShellProcessRmInfoEx 失败时的返回值
    if (dwNum == UINT(-1))
    {
        std::cerr << "GetShellProcessRmInfoEx failed." << std::endl;
        RmProcMemFree(lpRmProcList, lpdwCountNum);
        RmEndSession(dwSessionHandle);
        return DWORD(-1);
    }

    // 遍历进程信息数组
    std::cout << "Process Count: " << dwNum << std::endl;
    std::cout << "Shell PID: " << std::endl;
    for (UINT i = 0; i < dwNum; i++)
    {
        std::cout << " > " << lpRmProcList[i].dwProcessId << std::endl;
    }

    // 注册重启管理器信息
    dwRmStatus = RmRegisterResources(dwSessionHandle,
        0, NULL, dwNum, lpRmProcList, 0, NULL);
    if (ERROR_SUCCESS != dwRmStatus)
    {
        std::cerr << "RmRegisterResources failed: " << std::dec << dwRmStatus << std::endl;
        RmProcMemFree(lpRmProcList, lpdwCountNum);
        RmEndSession(dwSessionHandle);
        return DWORD(-1);
    }

    // 强制结束进程
    dwRmStatus = RmShutdown(dwSessionHandle, RmForceShutdown, RmWriteStatusCallback);
    if (ERROR_SUCCESS != dwRmStatus && ERROR_FAIL_SHUTDOWN != dwRmStatus)
    {
        std::cerr << "RmShutdown failed: " << std::dec << dwRmStatus << std::endl;
        RmEndSession(dwSessionHandle);
        return DWORD(-1);
    }

    // 关闭重启管理器会话
    dwRmStatus = RmEndSession(dwSessionHandle);
    if (ERROR_SUCCESS != dwRmStatus)
    {
        std::cerr << "RmEndSession failed: " << std::dec << dwRmStatus << std::endl;
        return DWORD(-1);
    }

    // 释放进程信息数组占用的缓冲区
    RmProcMemFree(lpRmProcList, lpdwCountNum);
    
    return ERROR_SUCCESS;
}

// RM 处理回调,显示任务完成的状态
void RmWriteStatusCallback(
    UINT nPercentComplete
)
{
    std::cout << "Task completion level: " << std::dec << nPercentComplete << std::endl;
}

BOOL GetShellProcessRmInfoEx(
    PRM_UNIQUE_PROCESS* lpRmProcList,
    DWORD_PTR* lpdwCountNum
)
{
    PROCESSENTRY32W pe32 = { 0 };
    FILETIME lpCreationTime = { 0 };
    FILETIME lpExitTime = { 0 };
    FILETIME lpKernelTime = { 0 };
    FILETIME lpUserTime = { 0 };
    HANDLE hProcess = nullptr;
    RM_UNIQUE_PROCESS tpProc = { 0 };
    std::vector<RM_UNIQUE_PROCESS> RmProcVec;
    SIZE_T VecLength = 0;

    // 在使用这个结构前,先设置它的大小
    pe32.dwSize = sizeof(pe32);

    // 给系统内所有的进程拍个快照
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE)
    {
        std::cerr << "CreateToolhelp32Snapshot 调用失败." << std::endl;
        return FALSE;
    }

    // 遍历进程快照,轮流显示每个进程的信息
    BOOL bMore = Process32FirstW(hProcessSnap, &pe32);
    while (bMore)
    {
        if (!_wcsicmp(pe32.szExeFile, L"explorer.exe")
            && pe32.cntThreads > 1)// 线程数大于 1,是为了过滤僵尸进程
        {
            hProcess = OpenProcess(
                PROCESS_QUERY_LIMITED_INFORMATION,
                FALSE, pe32.th32ProcessID);
            if (hProcess != nullptr)
            {
                memset(&lpCreationTime, 0, sizeof(FILETIME));
                // 获取创建时间
                if (GetProcessTimes(hProcess,
                    &lpCreationTime, &lpExitTime,
                    &lpKernelTime, &lpUserTime) == TRUE)
                {
                    tpProc.dwProcessId = pe32.th32ProcessID;
                    tpProc.ProcessStartTime = lpCreationTime;
                    RmProcVec.push_back(tpProc);
                }

                CloseHandle(hProcess);
                hProcess = nullptr;
            }
        }
        bMore = Process32NextW(hProcessSnap, &pe32);
    }

    // 清除 snapshot 对象
    CloseHandle(hProcessSnap);

    VecLength = RmProcVec.size();

    if (VecLength == 0)  // 没有找到进程
    {
        *lpdwCountNum = 0;
        *lpRmProcList = 0;
        return TRUE;
    }

    // 将进程信息复制到数组中
    if (VecLength < (SIZE_T)0xf4236u)
    {
        RM_UNIQUE_PROCESS* lprmUniqueProc =
            new(std::nothrow) RM_UNIQUE_PROCESS[VecLength];

        if (lprmUniqueProc != nullptr)
        {
            SIZE_T rSize = VecLength * sizeof(RM_UNIQUE_PROCESS);

            if (rSize < (SIZE_T)0xC80000u && rSize > 0)
            {
                if (!memcpy_s(lprmUniqueProc, rSize, &RmProcVec[0], rSize))
                {
                    *lpdwCountNum = VecLength;
                    *lpRmProcList = lprmUniqueProc;
                    return TRUE;
                }
            }
            else {
                std::cerr << "Vector Size to large!" << std::endl;
            }
        }
        else {
            std::cerr << "Alloc memory failed!" << std::endl;
        }
    }
    else {
        std::cerr << "Vector Size is invalid!" << std::endl;
    }
    return FALSE;
}

void RmProcMemFree(
    PRM_UNIQUE_PROCESS lpRmProcList,
    DWORD_PTR lpdwCountNum)
{
    __try
    {
        DWORD_PTR dwCountNum = lpdwCountNum;

        if (lpRmProcList != nullptr && dwCountNum > 0)
        {
            while (--dwCountNum)
            {
                if (IsBadWritePtr(&lpRmProcList[dwCountNum], sizeof(RM_UNIQUE_PROCESS)))
                {
                    throw(L"BadWritePtr event!");
                    break;
                }

                memset(&lpRmProcList[dwCountNum], 0, sizeof(RM_UNIQUE_PROCESS));
            }

            delete[] lpRmProcList;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        // 处理 SEH 异常
        std::cerr << "Error access violation." << std::endl;
        exit(-1);
    }
}
创建挂起进程注入执行结果

不过显然后释放远程线程有概率拦截不到一些初始化阶段的信息,并且挂起式注入在进程完成初始化之前的上下文中执行,这可能带来一些崩溃风险。

创建挂起进程注入有概率失败

通过多次调试,我发现在 x64dbg 调试器捕获到 explorer 启动时的第一个 TLS 回调事件时注入 explorer 进程,也可以顺利拦截所有托盘图标信息。推测此时已经过了保护阶段,explorer 准备加载不受保护的一些模块。

那么,我们可以去编写一个调试注入器,以便于在资源管理器启动的合适阶段完成注入。

下面将分析如何编写简单的调试注入器。

3.2 创建调试进程注入

调试器的 “创建调试进程” 功能一般通过 CreateProcess 函数来完成。在 CreateProcess 的 dwCreateFlags 参数中指定 DEBUG_ONLY_THIS_PROCESS 就可以只调试当前进程不调试子进程,CREATE_NEW_CONSOLE 将对应的控制台分离,防止受调试进程和当前进程共用控制台。

BOOL WINAPI OnInitCreateDebugExplorer(PDWORD lpProcessId)
{
    // 要调试的进程路径
    LPCWSTR lpApplicationName = L"C:\\Windows\\explorer.exe";

    // 启动进程并进入调试模式
    STARTUPINFOW si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));
    si.cb = sizeof(si);

    if (!CreateProcessW(lpApplicationName, NULL, NULL, NULL, FALSE,
        DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE,
        NULL, NULL, &si, &pi))
    {
        printf("Error: Failed to create debug process!\n");
        return FALSE;
    }

    *lpProcessId = pi.dwProcessId;
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return TRUE;
}

3.3 等待调试事件

我们使用 WaitForDebugEvent 循环来等待被调试进程发出的调试信息,在接收到的消息中使用自定义的回调函数处理调试事件。

// 处理调试事件
int dwStatus = 0;
DEBUG_EVENT debugEvent;
while (WaitForDebugEvent(&debugEvent, INFINITE))
{
    // 执行回调函数
    UINT uResponse = DebugEventCallback(debugEvent, lpStatusStruct);

    // 检查 switch-case 内是否指示了一个 break
    if (uResponse != DMSG_ERRORSUCCESS)
    {
        break;
    }
}

DEBUG_EVENT 结构体描述了调试事件的信息:

typedef struct _DEBUG_EVENT {
  DWORD dwDebugEventCode;
  DWORD dwProcessId;
  DWORD dwThreadId;
  union {
    EXCEPTION_DEBUG_INFO      Exception;
    CREATE_THREAD_DEBUG_INFO  CreateThread;
    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
    EXIT_THREAD_DEBUG_INFO    ExitThread;
    EXIT_PROCESS_DEBUG_INFO   ExitProcess;
    LOAD_DLL_DEBUG_INFO       LoadDll;
    UNLOAD_DLL_DEBUG_INFO     UnloadDll;
    OUTPUT_DEBUG_STRING_INFO  DebugString;
    RIP_INFO                  RipInfo;
  } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

其中,dwDebugEventCode 描述了调试事件的类型,总共有 9 类调试事件:

dwDebugEventCode事件类型

CREATE_PROCESS_DEBUG_EVENT

创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件。

CREATE_THREAD_DEBUG_EVENT

创建一个线程之后发送此类调试事件。

EXCEPTION_DEBUG_EVENT

发生异常时发送此类调试事件。

EXIT_PROCESS_DEBUG_EVENT

进程结束后发送此类调试事件。

EXIT_THREAD_DEBUG_EVENT

一个线程结束后发送此类调试事件。

LOAD_DLL_DEBUG_EVENT

装载一个DLL模块之后发送此类调试事件。

OUTPUT_DEBUG_STRING_EVENT

被调试进程调用OutputDebugString之类的函数时发送此类调试事件。

RIP_EVENT

发生系统调试错误时发送此类调试事件。

UNLOAD_DLL_DEBUG_EVENT

卸载一个DLL模块之后发送此类调试事件。

每种调试事件的详细信息都是放在不同的结构体中的,而结构体通过联合体 u 来记录,通过 u 的字段的名称可以很快地判断哪个字段与哪种事件关联。例如  CREATE_PROCESS_DEBUG_EVENT 调试事件的详细信息由 CreateProcessInfo 字段来记录。

此外,dwProcessId 和 dwThreadId 分别是触发调试事件的进程 ID 和线程 ID。一个调试器可能同时调试多个进程,而每个进程内又可能有多个线程,通过这两个字段就可以知道调试事件是从哪个进程的哪个线程触发的了。

3.4 处理调试事件

调试器通过 WaitForDebugEvent 函数获取调试事件,通过 ContinueDebugEvent 继续被调试进程的执行。ContinueDebugEvent 有三个参数,第一和第二个参数分别是进程 ID 和线程 ID,表示让指定进程内的指定线程继续执行。通常这是在一个循环中完成的,如下面的代码所示:

void OnProcessCreated(const CREATE_PROCESS_DEBUG_INFO*);
void OnThreadCreated(const CREATE_THREAD_DEBUG_INFO*);
void OnException(const EXCEPTION_DEBUG_INFO*);
void OnProcessExited(const EXIT_PROCESS_DEBUG_INFO*);
void OnThreadExited(const EXIT_THREAD_DEBUG_INFO*);
void OnOutputDebugString(const OUTPUT_DEBUG_STRING_INFO*);
void OnRipEvent(const RIP_INFO*);
void OnDllLoaded(const LOAD_DLL_DEBUG_INFO*);
void OnDllUnloaded(const UNLOAD_DLL_DEBUG_INFO*);

BOOL waitEvent = TRUE;
DEBUG_EVENT debugEvent;
while (waitEvent == TRUE && WaitForDebugEvent(&debugEvent, INFINITE)) {

    switch (debugEvent.dwDebugEventCode) {

        case CREATE_PROCESS_DEBUG_EVENT:
            OnProcessCreated(&debugEvent.u.CreateProcessInfo);
            break;

        case CREATE_THREAD_DEBUG_EVENT:
            OnThreadCreated(&debugEvent.u.CreateThread);
            break;

        case EXCEPTION_DEBUG_EVENT:
            OnException(&debugEvent.u.Exception);
            break;

        case EXIT_PROCESS_DEBUG_EVENT:
            OnProcessExited(&debugEvent.u.ExitProcess);
            waitEvent = FALSE;
            break;

        case EXIT_THREAD_DEBUG_EVENT:
            OnThreadExited(&debugEvent.u.ExitThread);
            break;

        case LOAD_DLL_DEBUG_EVENT:
            OnDllLoaded(&debugEvent.u.LoadDll);
            break;

        case UNLOAD_DLL_DEBUG_EVENT:
            OnDllUnloaded(&debugEvent.u.UnloadDll);
            break;

        case OUTPUT_DEBUG_STRING_EVENT:
            OnOutputDebugString(&debugEvent.u.DebugString);
            break;

        case RIP_EVENT:
             OnRipEvent(&debugEvent.u.RipInfo);
            break;

        default:
            std::wcout << TEXT("Unknown debug event.") << std::endl;
            break;
        }

    if (waitEvent == TRUE) {
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
    }
    else {
        break;
    }
}

通过上文的分析,我们只需要对 CREATE_PROCESS_DEBUG_EVENT 和 EXCEPTION_DEBUG_EVENT 进行处理即可。

在 CREATE_PROCESS_DEBUG_EVENT 也就是进程创建时需要解析 explorer 进程的所有 TLS 回调函数。关于 TLS 结构的原理解释可以参考下面两篇文章:

首先澄清一点,在内存中动态获取 TLS 回调函数列表和从文件映射中解析的方法是有很大区别的。我们采用简单些的内存动态分析:

#include <windows.h>
#include <stdio.h>

// 定义 TLS 回调函数
typedef void (WINAPI* PIMAGE_TLS_CALLBACK)(LPVOID, DWORD, LPVOID);

// TLS 回调函数表结构
typedef struct _IMAGE_TLS_CALLBACK_ENTRY {
    struct _IMAGE_TLS_CALLBACK_ENTRY* pNext;
    PIMAGE_TLS_CALLBACK pCallbackFunc;
    LPVOID pCallbackContext;
} IMAGE_TLS_CALLBACK_ENTRY, * PIMAGE_TLS_CALLBACK_ENTRY;

int main() {

    // 要解析的 PE 文件路径
    LPCWSTR peFilePath = L"C:\\Windows\\explorer.exe";
    SetLastError(0);

    // 将模块加载到内存
    HMODULE fileBaseAddress = LoadLibraryW(peFilePath);
    if (fileBaseAddress == NULL) {
        printf("Failed to map file into memory. Error code: %d\n", GetLastError());
        return 1;
    }

    // 获取 DOS 头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBaseAddress;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        printf("Invalid DOS signature\n");
        FreeLibrary(fileBaseAddress);
        return 1;
    }

    // 获取 NT 头
    PIMAGE_NT_HEADERS pNtHeaders = 
        (PIMAGE_NT_HEADERS)((DWORD_PTR)fileBaseAddress
            + pDosHeader->e_lfanew);

    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        printf("Invalid NT signature\n");
        FreeLibrary(fileBaseAddress);
        return 1;
    }

    // 获取 TLS 目录
    PIMAGE_TLS_DIRECTORY pTlsDirectory = 
        (PIMAGE_TLS_DIRECTORY)((DWORD_PTR)fileBaseAddress
        + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);

    // 获取回调函数表地址
    PIMAGE_TLS_CALLBACK_ENTRY pCallbackEntry = 
        (PIMAGE_TLS_CALLBACK_ENTRY)((DWORD_PTR)pTlsDirectory->AddressOfCallBacks
            - pDosHeader->e_lfanew);

    printf("> Address of CallBackList Entry: 0x%I64X.\n", (DWORD_PTR)pCallbackEntry);

    printf("> Address of CallBacks: \n");
    // 遍历回调函数表
    while (pCallbackEntry->pCallbackFunc != NULL) {
        // 输出回调函数地址
        printf("\t\t\t> 0x%I64X\n", (DWORD_PTR)pCallbackEntry->pCallbackFunc);
        pCallbackEntry++;
    }

    // 卸载模块
    FreeLibrary(fileBaseAddress);

    return 0;
}

运行的效果很好:

获取程序 TLS 回调函数地址的结果

当获取完所有的 TLS 函数地址后,我们需要对每一个地址入口位置写入软件断点。

程序可能执行任意一个它认为需要的 TLS 回调,我们在所有 TLS 可能进入的位置写入断点,就可以在程序执行到的第一个 TLS 回调开始前中断。我们可以在中断后,恢复所有断点处的原始字节,并进行远程线程注入。此时所有线程处于挂起状态,只需要 ContinueDebugEvent 释放进程,并使用 DebugActiveProcessStop 函数结束调试模式,使得调试器分离。这样,就完成了整个注入过程。

注入的处理过程如下(在新线程 ThreadProc 中执行远程线程注入,不影响在此线程进行的进程释放过程):

typedef struct __pThreadFunc {
    LPCWSTR pszDllFileName;
    HANDLE  hProcess;
}pThreadFunc;

UINT WINAPI OnInjectModuleHandler(
    DWORD dwProcessId,
    DWORD dwThreadId,
    HANDLE hProcess)
{
    SetLastError(0);
    DWORD lpExitCode = 0;
    std::wstring dllModuleName = MODULE_DLL_NAME; // DLL 模块名称
    std::wstring dllModuleFullPath = GetDllModuleFullPath(dllModuleName);
    wprintf(L"Full path of DLL module: %s\n", dllModuleFullPath.c_str());

    static pThreadFunc pfn = { 0 };
    pfn.hProcess = hProcess;
    pfn.pszDllFileName = dllModuleFullPath.c_str();
    // 创建线程并在其中执行 ThreadProc 函数
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)&pfn, 0, NULL);
    if (hThread == NULL)
    {
        // 处理无法创建线程的情况
        CloseHandle(hProcess);
        return ERROR_INITTHREAD;
    }

    if (!ContinueDebugEvent(
        dwProcessId,
        dwThreadId,
        DBG_CONTINUE
    ))
    {
        printf("Error: Continue Debug Event failed.\n");
        return ERROR_CONTINUE_DBG;
    }

    DebugActiveProcessStop(dwProcessId);

    // 等待线程结束
    WaitForSingleObject(hThread, INFINITE);

    GetExitCodeThread(hThread, &lpExitCode);
    if (lpExitCode != 0)
    {
        printf("ThreadProc > Error:%u\n", lpExitCode);
        // 关闭句柄
        CloseHandle(hThread);
        return ERROR_INJECTFAILED;
    }

    // 关闭句柄
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return DMSG_COMPLETED;
}

3.5 完整代码和测试结果

HOOK 模块代码见 2.5 节最后一段内容即可(已经更新为最新代码)。

完整的调试注入器代码如下:

#include <Windows.h>
#include <iostream>
#include <vector>
#include <string>
#include <map>

#define DMSG_ERRORSUCCESS       0
#define DMSG_ONEXITPROC         1
#define DMSG_ONTLSCALLBACK      2
#define DMSG_NOTHANDLED         3
#define DMSG_COMPLETED          4
#define ERROR_CONTINUE_DBG      0xC00001
#define ERROR_INJECTFAILED      0xC00002
#define ERROR_INITTHREAD        0xC00003

#define EXPLORER_ENDTASK_CODE   1
#define MODULE_DLL_NAME         L"GetMessageHook.dll"

// 全局变量,用于存储 TLS 回调函数地址
PVOID g_pTlsCallback = NULL;

typedef void* DEBUG_STATUS;

VOID* __fastcall AllocDMsgBuffer();
VOID __fastcall ReleaseDMsgBuffer(LPVOID lpBuffer);
errno_t __fastcall memset_s(void* v, rsize_t smax,
    int c, rsize_t n);

std::wstring GetModulePath();
std::wstring GetDllModuleFullPath(const std::wstring& dllModuleName);

BOOL WINAPI SetSoftwareBreakpoint(
    HANDLE hProcess,
    LPVOID lpAddress,
    PBYTE lpByteBuffer
);

BOOL WINAPI ReleaseSoftwareBreakpoint(
    HANDLE hProcess,
    LPVOID lpAddress,
    const BYTE cByteBuffer
);

BOOL WINAPI OnInitCreateDebugExplorer(
    PDWORD lpProcessId);

BOOL WINAPI OnCreateProcessEventHandler(
    HANDLE hProcess,
    LPVOID pImageBase,
    LPVOID lpTlsCallbackList,
    LPVOID lpByteBufferBase
);

UINT WINAPI OnDebugTlsEventHandler(
    HANDLE hProcess,
    const std::vector<DWORD_PTR>* lpBreakpoint,
    const std::vector<BYTE>* lpByteBuffer,
    LPVOID lpExceptionAddress
);

BOOL WINAPI InjectMouldeHandler(
    HANDLE hProcess,
    LPCWSTR pszDllFileName
);
DWORD WINAPI ThreadProc(LPVOID lpParam);

UINT WINAPI OnInjectModuleHandler(
    DWORD dwProcessId,
    DWORD dwThreadId,
    HANDLE hProcess
);

UINT CALLBACK DebugEventCallback(
    DEBUG_EVENT& debugEvent,
    DEBUG_STATUS lpStatus
);

typedef struct _WND_INFO
{
    HWND hWnd;
    DWORD dwPid;
    DWORD dwTid;
    std::wstring strClass;
    std::wstring strText;
}WND_INFO;

typedef struct __DEBUGMSGSTRUCT
{
    HANDLE hProcess;
    HANDLE hThread;
    LPVOID lpImageBase;
    //SIZE_T  cbBreakPoints;
    std::vector<DWORD_PTR>* lpBreakpoint;
    //SIZE_T  cbByte;
    std::vector<BYTE>* ctByteBuffer;
}DEBUGMSGSTRUCT, * PDEBUGMSGSTRUCT;

// TLS 回调函数表结构
typedef struct _IMAGE_TLS_CALLBACK_ENTRY {
    struct _IMAGE_TLS_CALLBACK_ENTRY* pNext;
    PIMAGE_TLS_CALLBACK pCallbackFunc;
    LPVOID pCallbackContext;
} IMAGE_TLS_CALLBACK_ENTRY, * PIMAGE_TLS_CALLBACK_ENTRY;


BOOL OnInitCheckShellProcess();

int main()
{
    _wsetlocale(LC_ALL, L".UTF8"); // 设置代码页

    DEBUGMSGSTRUCT* lpStatusStruct = nullptr;
    DWORD dwProcessId = 0;

    if (!OnInitCheckShellProcess())
    {
        printf("Error: The task cannot continue.\n");
        return -1;
    }

    // 为结构体分配内存
    lpStatusStruct = (DEBUGMSGSTRUCT*)AllocDMsgBuffer();

    if (lpStatusStruct == nullptr)
    {
        printf("Error: Alloc DMsg Buffer failed.\n");
        return -1;
    }

    // 创建调试进程
    if (!OnInitCreateDebugExplorer(&dwProcessId))
    {
        printf("Error: Init debuggie process failed.\n");
        return -1;
    }

    // 处理调试事件
    int dwStatus = 0;
    DEBUG_EVENT debugEvent;
    while (WaitForDebugEvent(&debugEvent, INFINITE))
    {
        // 执行回调函数
        UINT uResponse = DebugEventCallback(debugEvent, lpStatusStruct);

        // 检查 switch-case 内是否指示了一个 break
        if (uResponse != DMSG_ERRORSUCCESS)
        {
            break;
        }
    }

    // 释放分配给 DMSG 的内存
    ReleaseDMsgBuffer(lpStatusStruct);
    lpStatusStruct = nullptr;
    return dwStatus;
}

// 该函数用于终止 explorer 进程
BOOL OnInitCheckShellProcess()
{
    HWND hWnd = nullptr;
    HANDLE hProcess = nullptr;
    DWORD dwProcess = 0;
    BOOL dwStatus = 0;
    hWnd = GetShellWindow();

    SetLastError(0);

    if (hWnd != NULL)
    {
        GetWindowThreadProcessId(hWnd, &dwProcess);
        if (dwProcess)
        {
            hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcess);
            if (hProcess)
            {
                if (IDYES == MessageBoxW(GetConsoleWindow(),
                    L"EXPLORER is still running, we will restart it.",
                    L"NOTICE: Is restart explorer now?",
                    MB_APPLMODAL | MB_ICONINFORMATION | MB_YESNO))
                {
                    // 结束 explorer 进程
                    dwStatus = TerminateProcess(hProcess, EXPLORER_ENDTASK_CODE);

                    if (dwStatus)
                    {
                        wprintf(L"Terminate explorer process successfully.\n");
                        Sleep(3000);
                        return TRUE;
                    }
                    else {
                        wprintf(L"Terminate explorer process failed, error(%u).\n",
                            GetLastError());
                        return FALSE;
                    }
                }
                else {
                    wprintf(L"Cancelled restart explorer process.\n");
                    return FALSE;
                }
            }
            else {
                wprintf(L"Open explorer process failed, error(%u).\n",
                    GetLastError());
                return FALSE;
            }
        }
        else {
            wprintf(L"Get explorer process id failed, error(%u).\n",
                GetLastError());
            return FALSE;
        }
    }
    else {
        wprintf(L"Get shell windows handle failed, error(%u).\n",
            GetLastError());
        return FALSE;
    }

}


std::wstring GetModulePath()
{
    wchar_t modulePath[MAX_PATH];
    GetModuleFileName(NULL, modulePath, MAX_PATH);

    // 获取程序路径
    std::wstring fullPath(modulePath);

    // 去除程序名称,只保留路径
    size_t lastSlashIndex = fullPath.find_last_of(L"\\");
    std::wstring programPath = fullPath.substr(0, lastSlashIndex + 1);

    return programPath;
}

std::wstring GetDllModuleFullPath(const std::wstring& dllModuleName)
{
    std::wstring programPath = GetModulePath();

    // 合成完整的DLL模块路径
    std::wstring dllModuleFullPath = programPath + dllModuleName;

    return dllModuleFullPath;
}

// 调试事件处理函数
UINT CALLBACK DebugEventCallback(
    DEBUG_EVENT& debugEvent,
    DEBUG_STATUS lpStatus
)
{
    DWORD dwContinueStatus = DBG_CONTINUE;
    UINT uMsgResponse = 0;
    PDEBUGMSGSTRUCT lpMsgStruct = (PDEBUGMSGSTRUCT)lpStatus;

    switch (debugEvent.dwDebugEventCode)
    {
    case CREATE_PROCESS_DEBUG_EVENT:
    {
        PDEBUGMSGSTRUCT lpMsgStruct = (PDEBUGMSGSTRUCT)lpStatus;
        lpMsgStruct->hProcess = debugEvent.u.CreateProcessInfo.hProcess;
        lpMsgStruct->hThread = debugEvent.u.CreateProcessInfo.hThread;
        lpMsgStruct->lpImageBase = debugEvent.u.CreateProcessInfo.lpBaseOfImage;
        DWORD_PTR TlsCallback = 0, lpByteBuffer = 0;
        if (!OnCreateProcessEventHandler(lpMsgStruct->hProcess, lpMsgStruct->lpImageBase,
            &TlsCallback, &lpByteBuffer))
        {
            printf("Error: OnCreateProcessEventHandler failed.\n");
            uMsgResponse = 0xC00001;
            break;
        }

        //lpMsgStruct->cbBreakPoints = 1;
        //lpMsgStruct->cbByte = 1;
        lpMsgStruct->lpBreakpoint = (std::vector<DWORD_PTR>*)TlsCallback;
        lpMsgStruct->ctByteBuffer = (std::vector<BYTE>*)lpByteBuffer;

        break;
    }
    case EXCEPTION_DEBUG_EVENT:
    {
        // 判断是否为调试器引起的异常
        if (debugEvent.u.Exception.dwFirstChance == FALSE)
        {
            printf("Warnning: Exception First Chance!\n");
            // 返回 DBG_EXCEPTION_NOT_HANDLED 交给程序自身的 SEH 处理
            dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
            return DMSG_NOTHANDLED;
        }

        // 捕获到断点异常
        if (debugEvent.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
        {
            debugEvent.u.Exception.dwFirstChance += 1;
            const LPVOID lpExceptionAddress =
                debugEvent.u.Exception.ExceptionRecord.ExceptionAddress;
            const std::vector<DWORD_PTR>* lpBreakpoint = lpMsgStruct->lpBreakpoint;
            const std::vector<BYTE>* lpByteBuffer = lpMsgStruct->ctByteBuffer;
            HANDLE hProcess = lpMsgStruct->hProcess;

            uMsgResponse = OnDebugTlsEventHandler(hProcess, lpBreakpoint,
                lpByteBuffer, lpExceptionAddress);
        }
        break;
    }
    case EXIT_PROCESS_DEBUG_EVENT:
        uMsgResponse = DMSG_ONEXITPROC;
        break;
    default:
        dwContinueStatus = DBG_CONTINUE;
        // printf("dwDebugEventCode unHandled: %u\n", debugEvent.dwDebugEventCode);
        break;
    }

    // 判断接下来的操作
    if (uMsgResponse == DMSG_ERRORSUCCESS)  // 继续执行
    {
        if (!ContinueDebugEvent(
            debugEvent.dwProcessId,
            debugEvent.dwThreadId,
            dwContinueStatus
        ))
        {
            uMsgResponse = ERROR_CONTINUE_DBG;
        }
    }
    else if (uMsgResponse == DMSG_ONTLSCALLBACK)  // 准备注入
    {
        uMsgResponse = OnInjectModuleHandler(debugEvent.dwProcessId,
            debugEvent.dwThreadId, lpMsgStruct->hProcess);

        if (uMsgResponse == DMSG_COMPLETED) {  // 完成注入
            printf("Procedure Completed.\n");
        }
    }
    else {   // 调试异常,准备终止被调试进程
        TerminateProcess(lpMsgStruct->hProcess, 0);
    }

    return uMsgResponse;
}


BOOL WINAPI OnInitCreateDebugExplorer(PDWORD lpProcessId)
{
    // 要调试的进程路径
    LPCWSTR lpApplicationName = L"C:\\Windows\\explorer.exe";

    // 启动进程并进入调试模式
    STARTUPINFOW si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));
    si.cb = sizeof(si);

    if (!CreateProcessW(lpApplicationName, NULL, NULL, NULL, FALSE,
        DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE,
        NULL, NULL, &si, &pi))
    {
        printf("Error: Failed to create debug process!\n");
        return FALSE;
    }

    *lpProcessId = pi.dwProcessId;
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return TRUE;
}


BOOL WINAPI OnCreateProcessEventHandler(
    HANDLE hProcess,
    LPVOID pImageBase,
    LPVOID lpTlsCallbackList,
    LPVOID lpByteBufferBase
)
{
    static std::vector<DWORD_PTR> tlsCallbackList;
    static std::vector<BYTE> ctByteBuffer;
    BYTE lpByteBuffer = 0;
    IMAGE_DOS_HEADER pDosHeader = { 0 };
    ReadProcessMemory(hProcess, pImageBase, &pDosHeader, sizeof(IMAGE_DOS_HEADER), NULL);
    IMAGE_NT_HEADERS pNtHeaders = { 0 };
    ReadProcessMemory(hProcess, (LPVOID)((DWORD_PTR)pImageBase + pDosHeader.e_lfanew),
        &pNtHeaders, sizeof(IMAGE_NT_HEADERS), NULL);

    PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders.OptionalHeader;
    PIMAGE_DATA_DIRECTORY pDataDirectory = &pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
    IMAGE_TLS_DIRECTORY pTlsDirectory = { 0 };
    PIMAGE_TLS_CALLBACK_ENTRY pCallbackEntry = nullptr;
    IMAGE_TLS_CALLBACK_ENTRY tlsCallback = { 0 };

    BOOL ret = FALSE;

    // 获取 TLS 回调函数地址
    if (pDataDirectory->VirtualAddress != 0 && pDataDirectory->Size != 0)
    {
        ReadProcessMemory(hProcess, (LPVOID)((DWORD_PTR)pImageBase
            + pDataDirectory->VirtualAddress),
            &pTlsDirectory, sizeof(IMAGE_TLS_DIRECTORY), NULL);

        pCallbackEntry =
            (PIMAGE_TLS_CALLBACK_ENTRY)((DWORD_PTR)pTlsDirectory.AddressOfCallBacks - pDosHeader.e_lfanew);
        ReadProcessMemory(hProcess, (LPVOID)(pCallbackEntry),
            &tlsCallback, sizeof(tlsCallback), NULL);
        printf("TlsCallbackFuncEntry: 0x%I64X.\n", (DWORD_PTR)pCallbackEntry);

        while (tlsCallback.pCallbackFunc != nullptr) {

            tlsCallbackList.push_back((DWORD_PTR)tlsCallback.pCallbackFunc);
            pCallbackEntry++;
            memset_s(&tlsCallback, sizeof(tlsCallback), 0, sizeof(tlsCallback));
            ReadProcessMemory(hProcess, (LPVOID)(pCallbackEntry),
                &tlsCallback, sizeof(tlsCallback), NULL);
        }

        for (size_t i = 0; i < tlsCallbackList.size(); i++)
        {
            // 写入软件断点
            printf("Wrtie Software Breakpoint at: 0x%I64X\n", tlsCallbackList[i]);
            lpByteBuffer = 0;
            ret = SetSoftwareBreakpoint(hProcess, (LPVOID)tlsCallbackList[i], &lpByteBuffer);
            ctByteBuffer.push_back(lpByteBuffer);
        }

        // 将指针地址拷贝到结构体中
        DWORD_PTR tlsCallbaskListEntry = reinterpret_cast<DWORD_PTR>(&tlsCallbackList);
        DWORD_PTR ctByteBufferBase = reinterpret_cast<DWORD_PTR>(&ctByteBuffer);
        memcpy(lpTlsCallbackList, &tlsCallbaskListEntry, sizeof(DWORD_PTR));
        memcpy(lpByteBufferBase, &ctByteBufferBase, sizeof(DWORD_PTR));
    }
    return ret;
}


UINT WINAPI OnDebugTlsEventHandler(
    HANDLE hProcess,
    const std::vector<DWORD_PTR>* lpBreakpoint,
    const std::vector<BYTE>* lpByteBuffer,
    LPVOID lpExceptionAddress
)
{
    UINT dwStatus = 0;
    std::vector<DWORD_PTR> TlsCallbackList =
        *lpBreakpoint;
    std::vector<BYTE> ctOriByteBuffer = *lpByteBuffer;

    DWORD_PTR dwExceptionAddress =
        reinterpret_cast<DWORD_PTR>(lpExceptionAddress);

    for (size_t index = 0; index < TlsCallbackList.size(); index++)
    {
        if (dwExceptionAddress == TlsCallbackList[index])
        {
#ifdef _DEBUG
            MessageBoxW(NULL, L"执行到第一个 TLS 函数入口点", L"Debug 提示信息", MB_OK);
#endif // DEBUG
            printf("First tlsCallBack function address: 0x%I64X.\n", TlsCallbackList[index]);
            dwStatus = DMSG_ONTLSCALLBACK;
            break;
        }
    }

    if (dwStatus != DMSG_ONTLSCALLBACK)
    {
        printf("System Breakpoint at: 0x%I64X.\n", dwExceptionAddress);
    }
    else {
        for (size_t index = 0; index < TlsCallbackList.size(); index++)
        {
            LPVOID tlsCallbackFunc = reinterpret_cast<LPVOID>(TlsCallbackList[index]);
            // 恢复原始字节
            if (!ReleaseSoftwareBreakpoint(hProcess, tlsCallbackFunc,
                ctOriByteBuffer[index]))
            {
                printf("Release Breakpoint failed: 0x%I64X.\n", TlsCallbackList[index]);
                dwStatus = 0xC00002;
                break;
            }
            printf("Release Software Breakpoint at: 0x%I64X.\n", TlsCallbackList[index]);
        }
    }
    return dwStatus;
}

typedef struct __pThreadFunc {
    LPCWSTR pszDllFileName;
    HANDLE  hProcess;
}pThreadFunc;

UINT WINAPI OnInjectModuleHandler(
    DWORD dwProcessId,
    DWORD dwThreadId,
    HANDLE hProcess)
{
    SetLastError(0);
    DWORD lpExitCode = 0;
    std::wstring dllModuleName = MODULE_DLL_NAME; // DLL 模块名称
    std::wstring dllModuleFullPath = GetDllModuleFullPath(dllModuleName);
    wprintf(L"Full path of DLL module: %s\n", dllModuleFullPath.c_str());
    //LPCWSTR pszDllFileName = ; // 要注入的 DLL 文件路径

    static pThreadFunc pfn = { 0 };
    pfn.hProcess = hProcess;
    pfn.pszDllFileName = dllModuleFullPath.c_str();
    // 创建线程并在其中执行 ThreadProc 函数
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)&pfn, 0, NULL);
    if (hThread == NULL)
    {
        // 处理无法创建线程的情况
        CloseHandle(hProcess);
        return ERROR_INITTHREAD;
    }

    if (!ContinueDebugEvent(
        dwProcessId,
        dwThreadId,
        DBG_CONTINUE
    ))
    {
        printf("Error: Continue Debug Event failed.\n");
        return ERROR_CONTINUE_DBG;
    }

    DebugActiveProcessStop(dwProcessId);

    // 等待线程结束
    WaitForSingleObject(hThread, INFINITE);

    GetExitCodeThread(hThread, &lpExitCode);
    if (lpExitCode != 0)
    {
        printf("ThreadProc > Error:%u\n", lpExitCode);
        // 关闭句柄
        CloseHandle(hThread);
        return ERROR_INJECTFAILED;
    }

    // 关闭句柄
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return DMSG_COMPLETED;
}


DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    pThreadFunc* pfn = (pThreadFunc*)lpParam;

    // 在线程中执行注入函数
    if (!InjectMouldeHandler(pfn->hProcess, pfn->pszDllFileName))
    {
        printf("ThreadProc > Inject Hook Module failed.\n");
        return ERROR_MOD_NOT_FOUND;
    }

    printf("ThreadProc > Inject Hook Module Success!\n");
    // 线程结束,返回退出代码
    return ERROR_SUCCESS;  // 操作成功完成
}

BOOL WINAPI InjectMouldeHandler(
    HANDLE hTargetProcess,
    LPCWSTR pszDllFileName
)
{

    // 1.目标进程句柄
    if (hTargetProcess == nullptr || pszDllFileName == nullptr)
    {
        wprintf(L"Error: InvalidSyntax error from InjectMouldeHandler.\n");
        SetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }
    size_t pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
    SetLastError(0);
    // 2.在目标进程中申请空间
    LPVOID lpPathAddr = VirtualAllocEx(hTargetProcess, 0, pathSize,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (NULL == lpPathAddr)
    {
        wprintf(L"Error[%d]: Failed to apply memory in the target process!\n", GetLastError());
        return FALSE;
    }

    SetLastError(0);
    // 3.在目标进程中写入 Dll 路径
    if (FALSE == WriteProcessMemory(hTargetProcess, lpPathAddr,
        pszDllFileName, pathSize, NULL))
    {
        wprintf(L"Error[%d]: Failed to write module path in target process!\n", GetLastError());
        return FALSE;
    }

    SetLastError(0);
    // 4.加载 ntdll.dll
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    if (NULL == hNtdll)
    {
        wprintf(L"Error[%d]: Failed to load NTDLL.DLL!\n", GetLastError());
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    SetLastError(0);
    // 5.获取 LoadLibraryW 的函数地址, FARPROC 可以自适应 32 位与 64 位
    FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandleW(L"KernelBase.dll"),
        "LoadLibraryW");
    if (NULL == pFuncProcAddr)
    {
        wprintf(L"Error[%d]: Failed to obtain the address of the LoadLibrary function!\n",
            GetLastError());
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    // 6.获取 NtCreateThreadEx 函数地址,该函数在32位与64位下原型不同
    // _WIN64 用来判断编译环境 ,_WIN32用来判断是否是 Windows 系统
#ifdef _WIN64
    typedef DWORD(WINAPI* __NtCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown
        );
#else
    typedef DWORD(WINAPI* __NtCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown
        );
#endif

    SetLastError(0);
    __NtCreateThreadEx NtCreateThreadEx =
        (__NtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");
    if (NULL == NtCreateThreadEx)
    {
        wprintf(L"Error[%d]: Failed to obtain NtCreateThreadEx function address!\n",
            GetLastError());
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    SetLastError(0);
    // 7.在目标进程中创建远线程
    HANDLE hRemoteThread = NULL;
    DWORD lpExitCode = 0;
    DWORD dwStatus = NtCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
        hTargetProcess,
        (LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
    if (NULL == hRemoteThread)
    {
        wprintf(L"Error[%d]: Failed to create thread in target process!\n", GetLastError());
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    SetLastError(0);
    // 8.等待线程结束
    if (WAIT_TIMEOUT == WaitForSingleObject(hRemoteThread, INFINITE))
    {
        wprintf(L"Error[%d]: Remote thread not responding.\n", GetLastError());
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    GetExitCodeThread(hRemoteThread, &lpExitCode);
    if (lpExitCode == 0)
    {
        wprintf(L"Error: Injection module failed in the target process.\n");
        VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
        return FALSE;
    }

    // 9.清理环境
    VirtualFreeEx(hTargetProcess, lpPathAddr, 0, MEM_RELEASE);
    CloseHandle(hRemoteThread);
    return TRUE;
}


BOOL WINAPI SetSoftwareBreakpoint(
    HANDLE hProcess,
    LPVOID lpAddress,
    PBYTE lpByteBuffer
)
{
    DWORD oldProtect;
    BOOL vPret = FALSE;
    vPret = VirtualProtectEx(hProcess, lpAddress, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    if (vPret == FALSE)
    {
        return vPret;
    }
    vPret = ReadProcessMemory(hProcess, lpAddress, lpByteBuffer, 1, NULL); // 保存原始字节
    if (vPret == FALSE)
    {
        VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
        return vPret;
    }
    vPret = WriteProcessMemory(hProcess, lpAddress, "\xCC", 1, NULL); // \xCC 是 INT 3 指令,用于产生软件断点
    if (vPret == FALSE)
    {
        VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
        return vPret;
    }
    vPret = VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
    if (vPret == FALSE)
    {
        WriteProcessMemory(hProcess, lpAddress, lpByteBuffer, 1, NULL);
        return vPret;
    }

    return TRUE;
}

BOOL WINAPI ReleaseSoftwareBreakpoint(
    HANDLE     hProcess,
    LPVOID     lpAddress,
    const BYTE cByteBuffer
)
{
    DWORD oldProtect;
    BOOL vPret = FALSE;
    vPret = VirtualProtectEx(hProcess, lpAddress, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    if (vPret == FALSE)
    {
        return vPret;
    }
    vPret = WriteProcessMemory(hProcess, lpAddress, &cByteBuffer, 1, NULL); // 恢复原始字节
    if (vPret == FALSE)
    {
        VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
        return vPret;
    }
    vPret = VirtualProtectEx(hProcess, lpAddress, 1, oldProtect, &oldProtect);
    if (vPret == FALSE)
    {
        return vPret;
    }

    return TRUE;
}

errno_t __fastcall memset_s(void* v, rsize_t smax, int c, rsize_t n) {
    if (v == NULL) return EINVAL;
    if (smax > RSIZE_MAX) return EINVAL;
    if (n > smax) return EINVAL;
    volatile unsigned char* p = (volatile unsigned char*)v;
    while (smax-- && n--) {
        *p++ = c;
    }
    return 0;
}

VOID* __fastcall AllocDMsgBuffer()
{
    LPVOID lpStatusStruct = malloc(sizeof(DEBUGMSGSTRUCT));

    if (lpStatusStruct == nullptr)
    {
        return NULL;
    }

    if (EINVAL == memset_s(lpStatusStruct,
        sizeof(DEBUGMSGSTRUCT),
        0, sizeof(DEBUGMSGSTRUCT)))
    {
        free(lpStatusStruct);
        return NULL;
    }

    return lpStatusStruct;
}

VOID __fastcall ReleaseDMsgBuffer(LPVOID lpBuffer)
{
    if (lpBuffer == nullptr)
    {
        return;
    }

    if (EINVAL == memset_s(lpBuffer,
        sizeof(DEBUGMSGSTRUCT),
        0, sizeof(DEBUGMSGSTRUCT)))
    {

        return;
    }

    free(lpBuffer);
}

询问重启资源管理器:

询问是否重启资源管理器

P.S.:重启资源管理器有官方支持的方法,你可以在代码中改用重启管理器接口(Restart Manager API),可以见 4.1 的代码中就运用了 RM 重启 explorer 。

调试注入器可以成功注入 HOOK 模块:

调试注入器截图

记录的日志内容如下:

测试日志记录截图[新版]

3.6 补充说明

对于要在启动系统时使用该工具,则可以删除掉关闭进程前的询问环节。可以将程序编写为服务程序或者使用注册表。具体解释如下:

1)服务进程可以在系统登陆前启动,此时就可以重启并注入 explorer 进程。

2)使用映像文件执行选项并将注入器程序注册为“调试器”(有关详细信息,请参阅 “如何:自动启动调试器”)。

原理很简单:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 项下添加一个包含目标进程名称的项(例如 explorer.exe)。
在此键下,添加一个名为 debugger 的新字符串值。
将值设置为修改二进制文件的路径名。这必须是完全限定的路径名​​,或者图像位置必须位于 PATH 环境变量中。
每当启动 explorer.exe 时,注入器二进制文件都会在加载 explorer.exe(及其依赖项)之后、执行开始之前启动。无论 explorer.exe 如何启动,都会发生这种情况。

另请注意,在 64 位操作系统上,密钥也会反映在 Wow6432Node 中,因此注入器二进制文件将同时针对 32 位和 64 位版本的 explorer.exe 启动。

IFEO 调试启动

但是,调试启动需要处理一些情况:比如已经注入会话 explorer.exe 进程,对之后启动的多进程副本,不再重新重启并注入模块(需要考虑很多额外的情况,不要轻易尝试在这里使用我的原始代码)。

此外,似乎处理 x86, x64, wow 的方法是创建 ID 为 0 1 2 的文件夹:

处理不同处理器版本

4 初步分析系统托盘图标的注册表转储

4.1 传统的注册表转储信息

在 Win11 之前,系统通过 IconStreams 和 PastIconsStream(可能没有) 两个注册表值项获取 explorer 的通知图标信息,他们位于:

[HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify]

该注册表具有一个映射位置,一般我们修改上面的 UserKey :

[HKEY_CLASSES_ROOT\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify]

其中一个页面如下图所示:

TrayNotify 注册表

这里面一般只存储缓存数据,explorer 会在关闭时将内存中的数据写回到磁盘上的注册表中。

关于它的参数构成可以参考一篇外文博客:Windows 7 Notification Area Automation

目前网上大多数都是 PureBasic 和 PowerShell 脚本实现:

使用 C/Java 等代码的较少,而且多数脚本的使用并不是太方便,所以我结合多篇文献自己实现了一个修改图标可见性的命令提示符工具 SysTrayIconTool ,使用 C++ 风格的代码。(关于代码的具体讲解见第 3 篇文章)

参数功能说明:

  • 【/show】 或者 【/showall】:解码系统通知栏所有图标的注册表摘要信息。
  • 【/clear】 或者 【/clearreg】:清除注册表图标缓存信息(严重警告:该操作将彻底清空配置文件),仅限配置异常或冗余时使用。
  • 【/find】【PathName】:通过映像的绝对 Win32 路径匹配注册表信息。程序输出图标可见度级别信息。
  • 【/fuzzyfind】【PathName】:在不知道图标的完整路径情况下,允许模糊查找所有包含特征字符串的图标信息。
  • 【/exactsetval】【PathName】【0/1/2】:通过映像的 Win32 路径修改注册表中图标的可见性级别信息,支持系统文件和当前目录文件的相对路径。
  • 【/setvalfuzzy】【BaseName】【0/1/2】:允许在不知道图标完整路径的情况下批量修改包含特定字符串的图标信息。

程序需要提升管理员权限!完整代码如下:

/**********************************************************************************************************************
 *     ______   __  __   ______   _________  ______    ________   __  __   ________  ______   ______   ___   __
 *    /_____/\ /_/\/_/\ /_____/\ /________/\/_____/\  /_______/\ /_/\/_/\ /_______/\/_____/\ /_____/\ /__/\ /__/\
 *    \::::_\/_\ \ \ \ \\::::_\/_\__.::.__\/\:::_ \ \ \::: _  \ \\ \ \ \ \\__.::._\/\:::__\/ \:::_ \ \\::\_\\  \ \
 *     \:\/___/\\:\_\ \ \\:\/___/\  \::\ \   \:(_) ) )_\::(_)  \ \\:\_\ \ \  \::\ \  \:\ \  __\:\ \ \ \\:. `-\  \ \
 *      \_::._\:\\::::_\/ \_::._\:\  \::\ \   \: __ `\ \\:: __  \ \\::::_\/  _\::\ \__\:\ \/_/\\:\ \ \ \\:. _    \ \
 *        /____\:\ \::\ \   /____\:\  \::\ \   \ \ `\ \ \\:.\ \  \ \ \::\ \ /__\::\__/\\:\_\ \ \\:\_\ \ \\. \ `_  \ \
 *        \_____\/  \__\/   \_____\/   \__\/    \_\/ \_\/ \__\/\__\/  \__\/ \________\/ \_____\/ \_____\/ \__\/ \__\/
 *
 *      
 *      FileName:   SysTrayIconTool.cpp
 *      Author:     LianYou-516 
 *      Version:    1.0 
 *      Date:       2024/2/19
 *      Description:  This program is mainly used to modify the display status information
 *                    of the notification area icon on the taskbar. The search application's 
 *                    pattern supports fuzzy matching and exact matching, with a total of
 *                    three levels of display status, which are:
 * 
 *                           0 = Display only notifications
 *                           1 = Hide icons and notifications
 *                           2 = Display icons and notifications
 *                    Modify the notification area icon display level of the program
 *                    by specifying one of these three.
 * 
 * *********************************************************************************************************************
 */


#include <windows.h>
#include <iostream>
#include <memory>
#include <Knownfolders.h>
#include <ShlObj_core.h>
#include <Shlwapi.h>
#include <intsafe.h>
#include <cstdlib>
#include <restartmanager.h>
#include <TlHelp32.h>
#include <vector>

#pragma comment(lib, "Rstrtmgr.lib")
#pragma comment(lib, "Shlwapi.lib")

#define CS_DISPLAY_MODE         0x2L
#define CS_FULLNAME_MODE        0x4L
#define CS_BASENAME_MODE        0x8L
#define CS_CHANGESETTINGS       0x150L
#define TRAYNOTIFY_REG          L"Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\TrayNotify"
#define ICON_STREAM             L"IconStreams"
#define PAST_STREAM             L"PastIconsStream"
#define RM_SESSIONKEY_LEN sizeof(GUID) * 2 + 1

size_t wcslen_s(
    const wchar_t* str, size_t ccmaxlen
);

errno_t __fastcall memset_s(
    void* v,
    rsize_t smax,
    int c,
    rsize_t n
);

int CoCreateSuffixFullPath(
    wchar_t* wsBuffer,
    size_t wsCount,
    const wchar_t* wsFormat, ...
);

BOOL GetShellProcessRmInfoEx(
    PRM_UNIQUE_PROCESS* lpRmProcList,
    DWORD_PTR* lpdwCountNum
);
void RmProcMemFree(
    PRM_UNIQUE_PROCESS lpRmProcList,
    DWORD_PTR lpdwCountNum
);

void RmWriteStatusCallback(
    UINT nPercentComplete
);

DWORD SetShellRestartManager(
    DWORD dwSessionPID,
    BOOL bEnable,
    PRM_UNIQUE_PROCESS* lpRmStatus,
    DWORD_PTR* lpdwProcNum
);

std::wstring HexROT13(
    const std::wstring& text
);

PWSTR SHGetKnownFolderFullPath(
    LPCWSTR pszFullPath
);

PWSTR K32GetSystrayIconPath(
    LPCWSTR lpModuleName
);

BOOL ClearRegistryValue(
    HKEY hKey, 
    LPCWSTR subKey, 
    LPCWSTR valueName, 
    BOOL bWarnning
);

HRESULT FilePathFromKnownPrefix(
    LPCWSTR szKnownPath,
    PWSTR* szWin32FilePath
);

BYTE RegSystrayIconSettings(
    LPCWSTR lpszAppPath, 
    DWORD dwCSMode, 
    const BYTE dwVisable
);

BOOL OnClearTrayIconStream();
BYTE convertWcharToByte(wchar_t ch);
BOOL IsPathExist(LPCWSTR lpFilePath);

#include <Windows.h>
#include <tchar.h>

void SetConsoleSize(
    int cxPos, int cyPos,
    int nCols, int nLines
)
{
    HANDLE lpoptStdHandle = nullptr;
    CONSOLE_FONT_INFO consoleCurrentFont = { 0 };
    COORD bufferSize = { 0 }, fontSize = { 0 };
    TCHAR title[256];
    HWND hConsoleWnd = NULL;

    // Set console buffer size
    lpoptStdHandle = GetStdHandle(STD_OUTPUT_HANDLE);

    GetCurrentConsoleFont(lpoptStdHandle,
        false, &consoleCurrentFont);
    fontSize = GetConsoleFontSize(lpoptStdHandle,
        consoleCurrentFont.nFont);

    bufferSize.X = nCols;
    bufferSize.Y = nLines;
    SetConsoleScreenBufferSize(lpoptStdHandle, bufferSize);

    // Set console window size
    GetConsoleTitleW(title, 256);
    hConsoleWnd = FindWindowW(0, title);

    RECT consoleRect;
    GetWindowRect(hConsoleWnd, &consoleRect);

    // Check if the current size matches the desired size
    int desiredWidth = (nCols + 5) * fontSize.X;
    int desiredHeight = (nLines + 3) * fontSize.Y;

    if ((consoleRect.right - consoleRect.left < (desiredWidth - 25)) ||
        consoleRect.bottom - consoleRect.top < (desiredHeight - 25))
    {
        // 获取窗口的扩展样式
        LONG_PTR exStyle = GetWindowLongPtrW(hConsoleWnd, GWL_EXSTYLE);

        // 添加 WS_EX_LAYERED 扩展属性
        exStyle |= WS_EX_LAYERED;

        // 更新窗口的扩展样式
        SetWindowLongPtrW(hConsoleWnd, GWL_EXSTYLE, exStyle);

        // Fade out
        for (int alpha = 255; alpha >= 0; alpha -= 10)
        {
            SetLayeredWindowAttributes(hConsoleWnd, 0, alpha, LWA_ALPHA);
            UpdateWindow(hConsoleWnd);
            Sleep(15);
        }

        // Move and resize the window
        MoveWindow(hConsoleWnd, cxPos, cyPos,
            desiredWidth, desiredHeight, FALSE);

        // Fade in
        for (int alpha = 0; alpha <= 255; alpha += 10)
        {
            SetLayeredWindowAttributes(hConsoleWnd, 0, alpha, LWA_ALPHA);
            UpdateWindow(hConsoleWnd);
            Sleep(15);
        }
    }

    // Show the window
    ShowWindow(hConsoleWnd, SW_SHOW);
}

void SetConsoleFont(const WCHAR* fontName, int fontSize)
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    CONSOLE_FONT_INFOEX fontInfo = { 0 };
    fontInfo.cbSize = sizeof(CONSOLE_FONT_INFOEX);

    // 获取当前控制台字体信息
    GetCurrentConsoleFontEx(hConsole, FALSE, &fontInfo);

    // 设置字体族为微软雅黑
    wcscpy_s(fontInfo.FaceName, LF_FACESIZE, fontName);

    // 修改字号
    fontInfo.dwFontSize.Y = fontSize;

    // 设置新的字体信息
    SetCurrentConsoleFontEx(hConsole, FALSE, &fontInfo);
}

void OutputPrintLogo()
{
    // 设置控制台窗口尺寸以适应 LOGO 文字宽度
    SetConsoleFont(L"点阵字体", 18);
    SetConsoleSize(50, 30, 125, 42);
    system("cls");
    printf("     ______   __  __   ______   _________  ______    ________   __  __   ________  ______   ______   ___   __\n");
    printf("    /_____/\\ /_/\\/_/\\ /_____/\\ /________/\\/_____/\\  /_______/\\ /_/\\/_/\\ /_______/\\/_____/\\ /_____/\\ /__/\\ /__/\\ \n");
    printf("    \\::::_\\/_\\ \\ \\ \\ \\\\::::_\\/_\\__.::.__\\/\\:::_ \\ \\ \\::: _  \\ \\\\ \\ \\ \\ \\\\__.::._\\/\\:::__\\/ \\:::_ \\ \\\\::\\_\\\\  \\ \\ \n");
    printf("     \\:\\/___/\\\\:\\_\\ \\ \\\\:\\/___/\\  \\::\\ \\   \\:(_) ) )_\\::(_)  \\ \\\\:\\_\\ \\ \\  \\::\\ \\  \\:\\ \\  __\\:\\ \\ \\ \\\\:. `-\\  \\ \\ \n");
    printf("      \\_::._\\:\\\\::::_\\/ \\_::._\\:\\  \\::\\ \\   \\: __ `\\ \\\\:: __  \\ \\\\::::_\\/  _\\::\\ \\__\\:\\ \\/_/\\\\:\\ \\ \\ \\\\:. _    \\ \\  \n");
    printf("        /____\\:\\ \\::\\ \\   /____\\:\\  \\::\\ \\   \\ \\ `\\ \\ \\\\:.\\ \\  \\ \\ \\::\\ \\ /__\\::\\__/\\\\:\\_\\ \\ \\\\:\\_\\ \\ \\\\. \\ `_  \\ \\ \n");
    printf("        \\_____\\/  \\__\\/   \\_____\\/   \\__\\/    \\_\\/ \\_\\/ \\__\\/\\__\\/  \\__\\/ \\________\\/ \\_____\\/ \\_____\\/ \\__\\/ \\__\\/\n");
    printf("\n");

}

void OutputHelpStrings()
{
    Sleep(25);
    std::cout << "    ----------------------------------------  SysTrayIconTool Help  ------------------------------------------\n";
    std::cout << "\tCAUTION: This tool requires modifying the registry at runtime, which requires extra caution!\n"
        << "\tIf you are not familiar with what you are doing, please do not continue to use it!\n";
    Sleep(25);
    std::cout << "\t<Options>\n\n" 
        << "\t[/show] or [/showall]:\n"
        << "\t\tDecode the registry summary information of all icons in the system notification bar.\n\n";
    Sleep(25);
    std::cout << "\t[/clear] or [/clearreg]: CAUTION!!! \n"
        << "\t\tClear registry icon cache information.This operation will completely clear the configuration file.\n\n";
    Sleep(25);
    std::cout << "\t[/find][PathName]:\n"
        << "\t\tMatches registry information through the Win32 path of the image,\n"
        << "\t\t supporting relative paths for system files and current directory files.\n";
    std::cout << "\t\tProgram output icon visibility level information.\n\n";
    Sleep(25);
    std::cout << "\t[/fuzzyfind][BaseName]:\n"
        << "\t\tAllow fuzzy search of all icon information containing specific strings without knowing the\n"
        << "\t\t complete path of the icon.\n\n";
    Sleep(25);
    std::cout << "\t[/exactsetval][PathName][0/1/2]:\n"
        << "\t\tModify the visibility level information of icons in the registry through the\n"
        << "\t\t Win32 path of the image, supporting relative paths for system files and current directory files.\n\n";
    Sleep(25);
    std::cout << "\t[/setvalfuzzy][BaseName][0/1/2]:\n"
        << "\t\tAllow batch modification of icon information containing specific strings without knowing \n"
        << "\t\t the complete path of the icon.\n";
}

// 主函数
int wmain(int argc, wchar_t* argv[]) {
    _wsetlocale(LC_ALL, L".UTF8");  // 设置代码页以支持中文
    OutputPrintLogo();
    BYTE dwVisable = 0;
    BYTE response = -1;
    PWSTR szTrayAppPath = nullptr;
    DWORD dwSessionPID = 0;
    PRM_UNIQUE_PROCESS lpRmStatus = nullptr;
    DWORD_PTR lpdwProcNum = 0;
    int mainresult = -1;

    if (argc < 2 || argc > 4)
    {
        std::cerr << "Error invalid parameters." << std::endl;
        OutputHelpStrings();
        return mainresult;
    }

    std::wstring wsOption_v1 = argv[1];

    if (argc == 2)
    {
        if (wsOption_v1 == L"/showall" || wsOption_v1 == L"/show")
        {
            response = RegSystrayIconSettings(nullptr, CS_DISPLAY_MODE, dwVisable);

            if (response == dwVisable)
            {
                std::cout << "The task has been successfully completed." << std::endl;
                mainresult = 0;
            }
            else {
                std::cerr << "Task execution failed." << std::endl;
                mainresult = 1;
            }
        }
        else if (wsOption_v1 == L"/clearreg" || wsOption_v1 == L"/clear")
        {
            // 结束资源管理器进程
            dwSessionPID = SetShellRestartManager(0, 
                FALSE, &lpRmStatus, &lpdwProcNum);

            if (dwSessionPID == DWORD(-1))
            {
                std::cerr << "Task execution failed." << std::endl;
                mainresult = 2;
            }
            else {
                if (!OnClearTrayIconStream())
                {
                    std::cerr << "Task execution failed." << std::endl;
                    mainresult = 2;
                }
                else {
                    std::cout << "The task has been successfully completed." << std::endl;
                    mainresult = 0;
                }
            }

            if (lpdwProcNum != 0)
            {
                std::cout << "Restart Shell Process." << std::endl;
                // 尝试重启资源管理器
                SetShellRestartManager(dwSessionPID,
                    TRUE, &lpRmStatus, &lpdwProcNum);
            }
        }
    }
    else if(argc >= 3){
        std::wstring wsOption_v2 = argv[2];
        if (wsOption_v1 == L"/find")
        {
            // 获取完整路径
            szTrayAppPath = K32GetSystrayIconPath(argv[2]);

            // 执行注册表操作
            response = RegSystrayIconSettings(szTrayAppPath, 
                CS_FULLNAME_MODE, dwVisable);

            // 判断状态
            if (response == dwVisable)
            {
                std::cout << "The task has been successfully completed." << std::endl;
                mainresult = 0;
            }
            else {
                std::cerr << "Task execution failed." << std::endl;
                mainresult = 3;
            }
        }
        else if (wsOption_v1 == L"/fuzzyfind")
        {
            // 执行注册表操作
            response = RegSystrayIconSettings(argv[2], 
                CS_BASENAME_MODE, dwVisable);

            // 判断状态
            if (response == dwVisable)
            {
                std::cout << "The task has been successfully completed." << std::endl;
                mainresult = 0;
            }
            else {
                std::cerr << "Task execution failed." << std::endl;
                mainresult = 4;
            }
        }
        else if (wsOption_v1 == L"/exactsetval")
        {
            // 参数校验
            if (argv[3] != nullptr && argc == 4)
            {
                // 获取要设置的显示状态值,可以为 0,1,2
                dwVisable = convertWcharToByte(argv[3][0]);

                if (dwVisable < 3u)   // 0,1,2
                {
                    // 结束资源管理器进程
                    dwSessionPID = SetShellRestartManager(0, 
                        FALSE, &lpRmStatus, &lpdwProcNum);

                    if (dwSessionPID == DWORD(-1))
                    {
                        std::cerr << "Task execution failed." << std::endl;
                        mainresult = 2;
                    }
                    else {
                        // 获取 TrayIconPath 支持的完整路径
                        szTrayAppPath = K32GetSystrayIconPath(argv[2]);

                        // 执行注册表操作
                        response = RegSystrayIconSettings(szTrayAppPath,
                            CS_FULLNAME_MODE | CS_CHANGESETTINGS, dwVisable);

                        if (response == dwVisable)
                        {
                            std::cout << "The task has been successfully completed." << std::endl;
                            mainresult = 0;
                        }
                        else {
                            std::cerr << "Task execution failed." << std::endl;
                            mainresult = 5;
                        }
                    }

                    if (lpdwProcNum != 0)
                    {
                        std::cout << "Restart Shell Process." << std::endl;
                        // 尝试重启资源管理器
                        SetShellRestartManager(dwSessionPID,
                            TRUE, &lpRmStatus, &lpdwProcNum);
                    }
                }
            }
        }
        else if (wsOption_v1 == L"/setvalfuzzy")
        {
            // 参数校验
            if (argv[3] != nullptr && argc == 4)
            {
                // 获取要设置的显示状态值,可以为 0,1,2
                dwVisable = convertWcharToByte(argv[3][0]);

                if (dwVisable < 3u)   // 0,1,2
                {
                    // 结束资源管理器进程
                    dwSessionPID = SetShellRestartManager(0, 
                        FALSE, &lpRmStatus, &lpdwProcNum);

                    if (dwSessionPID == DWORD(-1))
                    {
                        std::cerr << "Task execution failed." << std::endl;
                        mainresult = 2;
                    }
                    else {
                        // 执行注册表操作
                        response = RegSystrayIconSettings(argv[2],
                            CS_BASENAME_MODE | CS_CHANGESETTINGS, dwVisable);

                        if (response == dwVisable)
                        {
                            std::cout << "The task has been successfully completed." << std::endl;
                            mainresult = 0;
                        }
                        else {
                            std::cerr << "Task execution failed." << std::endl;
                            mainresult = 6;
                        }
                    }

                    if (lpdwProcNum != 0)
                    {
                        std::cout << "Restart Shell Process." << std::endl;
                        // 尝试重启资源管理器
                        SetShellRestartManager(dwSessionPID,
                            TRUE, &lpRmStatus, &lpdwProcNum);
                    }
                }
            }
        }
    }

    // 释放存放路径需要的内存
    if (szTrayAppPath != nullptr)
    {
        CoTaskMemFree(szTrayAppPath);
        szTrayAppPath = nullptr;
    }
    
    if (mainresult == -1)
    {
        std::cerr << "Error invalid parameters." << std::endl;
        OutputHelpStrings();
    }

    return mainresult;
}


BYTE RegSystrayIconSettings(LPCWSTR lpszAppPath, DWORD dwCSMode, const BYTE dwVisable)
{
    HKEY hKey = nullptr;
    BYTE ret = -1;

    if ((dwCSMode & CS_CHANGESETTINGS) != 0 && dwVisable > 2)
    {
        std::wcerr << L"dwVisable is invalid, dwVisable must be (0, 1, 2)." << std::endl;
        return ret;
    }

    if ((dwCSMode & CS_DISPLAY_MODE) != 0 && (dwCSMode & (CS_FULLNAME_MODE |
        CS_BASENAME_MODE |
        CS_CHANGESETTINGS)) != 0)
    {
        std::wcerr << L"dwCSMode is invalid." << std::endl;
        return ret;
    }

    if ((dwCSMode & CS_FULLNAME_MODE) != 0 && (dwCSMode & CS_BASENAME_MODE) != 0)
    {
        std::wcerr << L"dwCSMode is invalid." << std::endl;
        return ret;
    }

    if (RegOpenKeyEx(HKEY_CURRENT_USER,
        TRAYNOTIFY_REG,
        0, KEY_READ | KEY_WRITE, &hKey) == ERROR_SUCCESS)
    {
        DWORD dwType = REG_BINARY, lpcbData = 0;
        if (RegQueryValueEx(hKey, ICON_STREAM, 0, &dwType, NULL, &lpcbData) == ERROR_SUCCESS) {
            LPBYTE lpData = new BYTE[lpcbData];
            if (RegQueryValueEx(hKey, ICON_STREAM, 0, &dwType, lpData, &lpcbData) == ERROR_SUCCESS) {
                // 解析注册表值
                DWORD headerSize = 20;
                DWORD itemSize = 1640;
                DWORD numItems = (lpcbData - headerSize) / itemSize;

                for (DWORD i = 0; i < numItems; ++i) {
                    LPBYTE itemData = lpData + headerSize + i * itemSize;
                    std::wstring exePath(reinterpret_cast<wchar_t*>(itemData), 528 / sizeof(wchar_t));
                    for (int i = 0; i < 528 / sizeof(wchar_t); ++i) {
                        if (exePath[i] == L'\0') {
                            exePath.resize(i);
                            break;
                        }
                    }
                    // 对可执行文件路径进行 ROT13 解密
                    std::wstring decryptedExePath = HexROT13(exePath);

                    PWSTR szWin32FilePath = nullptr;
                    HRESULT hResult = NOERROR;
                    hResult = FilePathFromKnownPrefix(decryptedExePath.c_str(),
                        &szWin32FilePath);


                    if ((dwCSMode & CS_DISPLAY_MODE) != 0)        // 打印所有结果
                    {
                        if (hResult != NOERROR || szWin32FilePath == nullptr)
                        {
                            std::wcout << L"Executable path: " << decryptedExePath << std::endl;
                        }
                        else {
                            std::wcout << L"Executable path: " << szWin32FilePath << std::endl;
                            
                            CoTaskMemFree(szWin32FilePath);
                        }
                        std::wcout << L"Current Visualization level: "
                            << std::dec << itemData[528] << std::endl;
                        ret = 0;
                    }
                    else if ((dwCSMode & CS_FULLNAME_MODE) != 0)  // 匹配完整路径
                    {
                        if (lpszAppPath == nullptr) break;

                        if (decryptedExePath == std::wstring(lpszAppPath))
                        {
                            if (hResult != NOERROR || szWin32FilePath == nullptr)
                            {
                                std::wcout << L"Executable path: " << decryptedExePath << std::endl;
                            }
                            else {
                                std::wcout << L"Executable path: " << szWin32FilePath << std::endl;
                                CoTaskMemFree(szWin32FilePath);
                            }

                            std::wcout << L"Current Visualization level: "
                                << std::dec << itemData[528] << std::endl;

                            if ((dwCSMode & CS_CHANGESETTINGS) != 0)
                            {
                                std::wcout << L"Set Visualization level to "
                                    << std::dec << dwVisable << std::endl;
                                // 找到应用程序,修改其可见性设置
                                itemData[528] = dwVisable;

                                // 将修改后的字节数组写回注册表
                                if (RegSetValueEx(hKey, ICON_STREAM,
                                    0, REG_BINARY, lpData, lpcbData) == ERROR_SUCCESS)
                                {
                                    ret = dwVisable;
                                }

                                break;
                            }
                            ret = dwVisable;
                            break;
                        }
                    }
                    else if ((dwCSMode & CS_BASENAME_MODE) != 0)   // 模糊匹配
                    {
                        if (lpszAppPath == nullptr) break;

                        size_t nPos = decryptedExePath.find(lpszAppPath);
                        if (nPos != std::string::npos)
                        {
                            if (hResult != NOERROR || szWin32FilePath == nullptr)
                            {
                                std::wcout << L"Executable path: " << decryptedExePath << std::endl;
                            }
                            else {
                                std::wcout << L"Executable path: " << szWin32FilePath << std::endl;
                                CoTaskMemFree(szWin32FilePath);
                            }

                            std::wcout << L"Current Visualization level: "
                                << std::dec << itemData[528] << std::endl;

                            if ((dwCSMode & CS_CHANGESETTINGS) != 0)
                            {
                                std::wcout << L"Set Visualization level to "
                                    << std::dec << dwVisable << std::endl;

                                // 找到应用程序,修改其可见性设置
                                itemData[528] = dwVisable;

                                // 将修改后的字节数组写回注册表
                                if (RegSetValueEx(hKey, ICON_STREAM,
                                    0, REG_BINARY, lpData, lpcbData) == ERROR_SUCCESS)
                                {
                                    ret = dwVisable;
                                }
                                //break;
                            }
                            ret = dwVisable;
                            //break;
                        }
                    }
                    else {
                        std::wcerr << L"dwCSMode is invalid." << std::endl;
                        break;
                    }

                }
            }
            delete[] lpData;
        }
        RegCloseKey(hKey);
    }
    else {
        std::cerr << "Failed to open registry key." << std::endl;
        return ret;
    }

    return ret;
}

// ROT13 加密/解密算法
std::wstring HexROT13(const std::wstring& text) {
    std::wstring result;
    for (wchar_t c : text) {
        if (iswalpha(c)) {
            wchar_t base = iswupper(c) ? L'A' : L'a';
            result += (((c - base) + 13) % 26) + base;
        }
        else {
            result += c;
        }
    }
    return result;
}

// 控制 Explorer 重启状态
DWORD SetShellRestartManager(
    DWORD dwSessionPID, 
    BOOL bEnable, 
    PRM_UNIQUE_PROCESS* lpRmStatus, 
    DWORD_PTR* lpdwProcNum
)
{
    DWORD dwRmStatus = 0;
    DWORD dwSessionHandle = dwSessionPID;
    WCHAR strSessionKey[RM_SESSIONKEY_LEN] = { 0 };
    PRM_UNIQUE_PROCESS lpRmProcList = *lpRmStatus;
    DWORD_PTR lpdwCountNum = *lpdwProcNum;

    if (lpRmProcList == nullptr)
    {
        dwRmStatus = RmStartSession(&dwSessionHandle, NULL, strSessionKey);
        if (ERROR_SUCCESS != dwRmStatus)
        {
            std::cerr << "RmStartSession failed: " << std::dec << dwRmStatus << std::endl;
            return DWORD(-1);
        }
    }

    if (!bEnable)
    {
        if (!GetShellProcessRmInfoEx(&lpRmProcList, &lpdwCountNum))
        {
            std::cerr << "GetShellProcessRmInfoEx failed." << std::endl;
            RmEndSession(dwSessionHandle);
            return DWORD(-1);
        }

        // 传出结果
        *lpRmStatus = lpRmProcList;
        *lpdwProcNum = lpdwCountNum;

        UINT dwNum = static_cast<UINT>(lpdwCountNum);

        if (dwNum == UINT(0))  // 没有找到进程
        {
            std::cerr << "There are no shell processes that need to be closed." << std::endl;
            return dwSessionHandle;
        }

        if (dwNum == UINT(-1))
        {
            std::cerr << "GetShellProcessRmInfoEx failed." << std::endl;
            RmProcMemFree(lpRmProcList, lpdwCountNum);
            RmEndSession(dwSessionHandle);
            return DWORD(-1);
        }

        std::cout << "Process Count: " << dwNum << std::endl;
        std::cout << "Shell PID: " << std::endl;
        for (UINT i = 0; i < dwNum; i++)
        {
            std::cout << " > " << lpRmProcList[i].dwProcessId << std::endl;
        }

        dwRmStatus = RmRegisterResources(dwSessionHandle,
            0, NULL, dwNum, lpRmProcList, 0, NULL);
        if (ERROR_SUCCESS != dwRmStatus)
        {
            std::cerr << "RmRegisterResources failed: " << std::dec << dwRmStatus << std::endl;
            RmProcMemFree(lpRmProcList, lpdwCountNum);
            RmEndSession(dwSessionHandle);
            return DWORD(-1);
        }


        dwRmStatus = RmShutdown(dwSessionHandle, RmForceShutdown, RmWriteStatusCallback);
        if (ERROR_SUCCESS != dwRmStatus && ERROR_FAIL_SHUTDOWN != dwRmStatus)
        {
            std::cerr << "RmShutdown failed: " << std::dec << dwRmStatus << std::endl;
            RmEndSession(dwSessionHandle);
            return DWORD(-1);
        }
    }
    else {
        // 检查参数不为空
        if (lpdwCountNum == 0)
            return dwSessionHandle;

        dwRmStatus = RmRestart(dwSessionHandle, 0, RmWriteStatusCallback);
        if (ERROR_SUCCESS != dwRmStatus)
        {
            std::cerr << "RmRestart failed: " << std::dec << dwRmStatus << std::endl;
            RmEndSession(dwSessionHandle);
            return DWORD(-1);
        }

        dwRmStatus = RmEndSession(dwSessionHandle);
        if (ERROR_SUCCESS != dwRmStatus)
        {
            std::cerr << "RmEndSession failed: " << std::dec << dwRmStatus << std::endl;
            return DWORD(-1);
        }
        RmProcMemFree(lpRmProcList, lpdwCountNum);
        // 传出结果
        *lpRmStatus = nullptr;
        *lpdwProcNum = 0;
    }
    return dwSessionHandle;
}

void RmWriteStatusCallback(
    UINT nPercentComplete
)
{
    std::cout << "Task completion level: " << std::dec << nPercentComplete << std::endl;
}

BOOL GetShellProcessRmInfoEx(
    PRM_UNIQUE_PROCESS* lpRmProcList,
    DWORD_PTR* lpdwCountNum
)
{
    PROCESSENTRY32W pe32 = { 0 };
    FILETIME lpCreationTime = { 0 };
    FILETIME lpExitTime = { 0 };
    FILETIME lpKernelTime = { 0 };
    FILETIME lpUserTime = { 0 };
    HANDLE hProcess = nullptr;
    RM_UNIQUE_PROCESS tpProc = { 0 };
    std::vector<RM_UNIQUE_PROCESS> RmProcVec;
    SIZE_T VecLength = 0;

    // 在使用这个结构前,先设置它的大小
    pe32.dwSize = sizeof(pe32);

    // 给系统内所有的进程拍个快照
    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE)
    {
        std::cerr << "CreateToolhelp32Snapshot 调用失败." << std::endl;
        return FALSE;
    }

    // 遍历进程快照,轮流显示每个进程的信息
    BOOL bMore = Process32FirstW(hProcessSnap, &pe32);
    while (bMore)
    {
        if (!_wcsicmp(pe32.szExeFile, L"explorer.exe")
            && pe32.cntThreads > 1)// 线程数大于 1,是为了过滤僵尸进程
        {
            hProcess = OpenProcess(
                PROCESS_QUERY_LIMITED_INFORMATION,
                FALSE, pe32.th32ProcessID);
            if (hProcess != nullptr)
            {
                memset(&lpCreationTime, 0, sizeof(FILETIME));

                if (GetProcessTimes(hProcess,
                    &lpCreationTime, &lpExitTime,
                    &lpKernelTime, &lpUserTime) == TRUE)
                {
                    tpProc.dwProcessId = pe32.th32ProcessID;
                    tpProc.ProcessStartTime = lpCreationTime;
                    RmProcVec.push_back(tpProc);
                }

                CloseHandle(hProcess);
                hProcess = nullptr;
            }
        }
        bMore = Process32NextW(hProcessSnap, &pe32);
    }

    // 清除 snapshot 对象
    CloseHandle(hProcessSnap);

    VecLength = RmProcVec.size();

    if (VecLength == 0)  // 没有找到进程
    {
        *lpdwCountNum = 0;
        *lpRmProcList = 0;
        return TRUE;
    }

    if (VecLength < (SIZE_T)0xf4236u)
    {
        RM_UNIQUE_PROCESS* lprmUniqueProc =
            new(std::nothrow) RM_UNIQUE_PROCESS[VecLength];

        if (lprmUniqueProc != nullptr)
        {
            SIZE_T rSize = VecLength * sizeof(RM_UNIQUE_PROCESS);

            if (rSize < (SIZE_T)0xC80000u && rSize > 0)
            {
                if (!memcpy_s(lprmUniqueProc, rSize, &RmProcVec[0], rSize))
                {
                    *lpdwCountNum = VecLength;
                    *lpRmProcList = lprmUniqueProc;
                    return TRUE;
                }
            }
            else {
                std::cerr << "Vector Size to large!" << std::endl;
            }
        }
        else {
            std::cerr << "Alloc memory failed!" << std::endl;
        }
    }
    else {
        std::cerr << "Vector Size is invalid!" << std::endl;
    }
    return FALSE;
}

void RmProcMemFree(
    PRM_UNIQUE_PROCESS lpRmProcList,
    DWORD_PTR lpdwCountNum)
{
    __try
    {
        DWORD_PTR dwCountNum = lpdwCountNum;

        if (lpRmProcList != nullptr && dwCountNum > 0)
        {
            while (--dwCountNum)
            {
                if (IsBadWritePtr(&lpRmProcList[dwCountNum], sizeof(RM_UNIQUE_PROCESS)))
                {
                    throw(L"BadWritePtr event!");
                    break;
                }

                memset(&lpRmProcList[dwCountNum], 0, sizeof(RM_UNIQUE_PROCESS));
            }

            delete[] lpRmProcList;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        // 处理 SEH 异常
        std::cerr << "Error access violation." << std::endl;
        exit(-1);
    }
}

BOOL OnClearTrayIconStream()
{
    BOOL bSuccess = FALSE;

    // Clear PastIconsStream value
    bSuccess = ClearRegistryValue(HKEY_CURRENT_USER,
        TRAYNOTIFY_REG,
        PAST_STREAM, FALSE);

    if (bSuccess)
        std::cout << "PastIconsStream value cleared successfully." << std::endl;

    // Clear IconStreams value
    bSuccess = ClearRegistryValue(HKEY_CURRENT_USER,
        TRAYNOTIFY_REG,
        ICON_STREAM, TRUE);

    if (bSuccess)
        std::cout << "IconStreams value cleared successfully." << std::endl;

    return bSuccess;
}


BOOL ClearRegistryValue(HKEY hKey, LPCWSTR subKey, LPCWSTR valueName, BOOL bWarnning)
{
    HKEY hSubKey;
    LONG result = RegOpenKeyExW(hKey, subKey, 0, KEY_SET_VALUE, &hSubKey);
    if (result != ERROR_SUCCESS)
    {
        if (bWarnning)
        {
            std::cout << "Failed to open registry key: " << subKey << std::endl;
            return FALSE;
        }
        return TRUE;
    }

    result = RegDeleteValueW(hSubKey, valueName);
    if (result != ERROR_SUCCESS && bWarnning)
    {
        if (bWarnning)
        {
            std::cout << "Failed to delete registry value: " << valueName << std::endl;
            RegCloseKey(hSubKey);
            return false;
        }
        return TRUE;
    }

    RegCloseKey(hSubKey);
    return TRUE;
}



PWSTR K32GetSystrayIconPath(LPCWSTR lpModuleName)
{
    if (lpModuleName == nullptr)
    {
        std::wcerr << L"K32GetSystrayIconPath failed> Error null pointer." << std::endl;
        return nullptr;
    }

    if (lpModuleName[0] == L'\0')
    {
        std::wcerr << L"K32GetSystrayIconPath failed> Error null buffer." << std::endl;
        return nullptr;
    }

    // 动态分配内存存储程序文件的完整路径
    DWORD bufferSize = MAX_PATH;
    DWORD cbBufferSize = MAX_PATH;
    std::unique_ptr<wchar_t[]> buffer(new wchar_t[bufferSize]);

    if (PathIsRelativeW(lpModuleName) == TRUE)
    {
        // 查询程序文件的完整路径
        DWORD pathLength = SearchPathW(NULL, lpModuleName, NULL, bufferSize, buffer.get(), NULL);

        if (pathLength == 0) {
            // 查询失败,输出错误信息
            DWORD error = GetLastError();
            std::cerr << "Error searching for file path: " << error << std::endl;
            return nullptr;
        }

        // 如果缓冲区大小不够,重新分配内存并查询路径
        if (pathLength >= bufferSize) {
            bufferSize = pathLength + 1; // 考虑字符串末尾的空字符
            buffer.reset(new wchar_t[bufferSize]);

            pathLength = SearchPathW(NULL, lpModuleName, NULL, bufferSize, buffer.get(), NULL);
            if (pathLength == 0) {
                // 查询失败,输出错误信息
                DWORD error = GetLastError();
                std::cerr << "Error searching for file path: " << error << std::endl;
                return nullptr;
            }
        }

        return SHGetKnownFolderFullPath(buffer.get());
    }
    else if(!IsPathExist(lpModuleName)){
        std::wcerr << "Error file path not exist." << std::endl;
        return nullptr;
    }

    // 对路径进行修改,以便于支持 KNOWNFOLDERID 
    //PWSTR pszknFullPath;
    return SHGetKnownFolderFullPath(lpModuleName);
}

BOOL IsPathExist(LPCWSTR lpFilePath)
{
    WIN32_FILE_ATTRIBUTE_DATA attrs = { 0 };
    return 0 != GetFileAttributesExW(lpFilePath, GetFileExInfoStandard, &attrs);
}

PWSTR SHGetKnownFolderFullPath(LPCWSTR pszFullPath)
{
    if (pszFullPath == nullptr)
    {
        std::wcerr << L"SHGetKnownFolderFullPath failed> Error null pointer." << std::endl;
        return nullptr;
    }

    if (pszFullPath[0] == L'\0')
    {
        std::wcerr << L"SHGetKnownFolderFullPath failed> Error null buffer." << std::endl;
        return nullptr;
    }

#define WSGUID_STRING_LEN    76
#define GUID_STRING_LEN      38
    UINT index = 0;
    bool fflag = false;
    int nPosPrefix = -1, nNewPathRealLen = -1;
    PWSTR pszPath = nullptr, pszNewPath = nullptr;
    WCHAR szguid[WSGUID_STRING_LEN + 1] = { 0 };
    GUID EncodeCommonRelativePath[5] = { 0 };
    SIZE_T wsNewPathSize = 0;
    SIZE_T szwcharSize = sizeof(WCHAR);
    SIZE_T cbNewPathSize = 0;

    // 需要检查的 KNOWNFOLDERID 列表
    EncodeCommonRelativePath[0] = FOLDERID_ProgramFilesX86;
    EncodeCommonRelativePath[1] = FOLDERID_ProgramFilesX64;
    EncodeCommonRelativePath[2] = FOLDERID_SystemX86;
    EncodeCommonRelativePath[3] = FOLDERID_System;
    EncodeCommonRelativePath[4] = FOLDERID_Windows;

    // 查找并转换路径前缀
    while (index < 5)
    {
        if (SHGetKnownFolderPath(            // 该函数检查 KNOWNFOLDERID 对应的完整路径
            EncodeCommonRelativePath[index],
            KF_FLAG_DONT_VERIFY | KF_FLAG_NO_ALIAS,
            0,
            &pszPath) >= 0)
        {
            nPosPrefix = PathCommonPrefixW(pszPath, pszFullPath, 0);

            if (nPosPrefix && nPosPrefix == wcslen_s(pszPath, 0x800))
            {
                memset_s(szguid, sizeof(szguid), 0, sizeof(szguid));
                // 转换为 GUID 字符串
                if (!StringFromGUID2(EncodeCommonRelativePath[index], szguid, sizeof(szguid) / sizeof(*szguid)))
                {
                    std::wcerr << L"Error buffer to small." << std::endl;
                    break;
                }
                else {
                    // 计算将 GUID 前缀和路径后缀拼接成新的完整路径需要的字符数
                    wsNewPathSize = (GUID_STRING_LEN + wcslen_s(pszFullPath, 0x800) - nPosPrefix + 1);
                    // 安全地进行乘法运算,以便于获得需要分配的缓冲区字节数
                    if (SizeTMult(wsNewPathSize, szwcharSize, &cbNewPathSize) != S_OK) {
                        // 乘法溢出,处理错误
                        std::wcerr << L"Multiplication overflow occurred." << std::endl;
                        break;
                    }

                    // 分配生成字符串需要的缓冲区
                    pszNewPath = (PWSTR)CoTaskMemAlloc(cbNewPathSize);
                    if (pszNewPath == nullptr)
                    {
                        std::wcerr << L"Error not enough memory." << std::endl;
                        break;
                    }

                    // 初始化为 0
                    memset_s(pszNewPath, cbNewPathSize, 0, cbNewPathSize);

                    // 格式化为完整路径
                    nNewPathRealLen = CoCreateSuffixFullPath(
                        pszNewPath, wsNewPathSize,
                        L"%s%s", szguid,
                        &pszFullPath[nPosPrefix]
                    );

                    // 检查格式化的字符数
                    if (nNewPathRealLen <= 0 || nNewPathRealLen != wsNewPathSize)
                    {
                        std::wcerr << L"Error invalid suffix path." << std::endl;
                        CoTaskMemFree(pszNewPath);
                        pszNewPath = nullptr;
                        break;
                    }
                    else {
                        std::wcout << L"GetKnownFolderFullPath success!" << std::endl;
                        fflag = true;
                        break;
                    }
                }
            }
            // 没有找到匹配的目标前,每遍历一次需要释放占用的缓冲区
            CoTaskMemFree(pszPath);
            pszPath = nullptr;
        }
        ++index;
    }

    // 循环中使用 break 跳出时,过程中分配的缓冲区可能没有释放
    if (pszPath != nullptr)
    {
        CoTaskMemFree(pszPath);
        pszPath = nullptr;
    }

    if (!fflag)  // 没有找到时候直接复制输入缓冲区到输出缓冲区
    {
        size_t cbPathSize = wcslen_s(pszFullPath, 0x800);
        size_t wsPathSize = 0;
        if (cbPathSize > 0)
        {
            ++cbPathSize;
            // 获得需要分配的缓冲区字节数
            if (SizeTMult(cbPathSize, szwcharSize, &wsPathSize) == S_OK) {
                pszNewPath = (PWSTR)CoTaskMemAlloc(wsPathSize); // 分配缓冲区
                if (pszNewPath == nullptr)
                {
                    std::wcerr << L"Error not enough memory." << std::endl;
                    return pszNewPath;
                }
                memset_s(pszNewPath, wsPathSize, 0, wsPathSize);
                memcpy_s(pszNewPath, wsPathSize, pszFullPath, wsPathSize);
            }
        }
    }

    return pszNewPath;
#undef GUID_STRING_LEN
#undef WSGUID_STRING_LEN
}


/*
* 已知路径转换函数
*
* 参数:LPCWSTR szKnownPath 包含 GUID 前缀的完整路径
*       PWSTR szWin32FilePath 返回 Win32 完整路径
*
* ********************************************************************************************
* 备注:
*
* {F38BF404-1D43-42F2-9305-67DE0B28FC23} 表示 C:\Windows
* {F38BF404-1D43-42F2-9305-67DE0B28FC23}\explorer.exe 将被转换为 C:\Windows\explorer.exe
*
* SystemRoot = "{F38BF404-1D43-42F2-9305-67DE0B28FC23}";//SystemRoot:Windows Folder
* System32 = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}";//SystemRoot:Windows\\System32 Folder
* Program86 = "{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}";//SystemRoot:Program Files (x86) Folder
* Program = "{6D809377-6AF0-444B-8957-A3773F02200E}";//SystemRoot:Program Files Folder
*
* ********************************************************************************************
*/
HRESULT FilePathFromKnownPrefix(
    LPCWSTR szKnownPath,
    PWSTR* szWin32FilePath
)
{

    GUID stGuid = { 0 };
    PWSTR strPathPrefix = nullptr;
    PWSTR strWin32Path = nullptr;
    HRESULT str2GuidReslt = E_FAIL;
    std::wstring wsKnownPath;
    size_t nPos = std::string::npos;
    size_t nPrefix = 0,
        nKnownPath = 0,
        nWin32Path = 0,
        wsWin32PathLen = 0;
    int nCoResponse = -1;

    if (szKnownPath == nullptr)
    {
        SetLastError(ERROR_INVALID_PARAMETER);
        return str2GuidReslt;
    }

    if (szKnownPath[0] == L'\0')
    {
        SetLastError(ERROR_INVALID_PARAMETER);
        return str2GuidReslt;
    }

    wsKnownPath = szKnownPath;

    nPos = wsKnownPath.find_first_of(L'\\');

    if (nPos != 0x26u)       // GUID String 长度为 38 字符
    {
        SetLastError(ERROR_PATH_NOT_FOUND);
        return str2GuidReslt;
    }

    wsKnownPath.resize(0x26u);

    SetLastError(0);

    str2GuidReslt = CLSIDFromString((LPCOLESTR)wsKnownPath.c_str(), (LPCLSID)&stGuid);

    if (str2GuidReslt == (HRESULT)NOERROR) {
        //printf("The CLSID was obtained successfully.\n");
        str2GuidReslt = SHGetKnownFolderPath(stGuid, KF_FLAG_DONT_VERIFY, NULL, &strPathPrefix);

        if (SUCCEEDED(str2GuidReslt) && strPathPrefix != nullptr)
        {
            nPrefix = wcslen_s(strPathPrefix, 0x800u);
            nKnownPath = wcslen_s(szKnownPath, 0x800u);

            if (nPrefix == 0 || nKnownPath == 0)
            {
                std::wcerr << L"Get string length faild." << std::endl;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return E_FAIL;
            }

            nWin32Path = nKnownPath - 0x26u + nPrefix + 1;

            // 计算需要分配的缓冲区字节数
            if (SizeTMult(nWin32Path, sizeof(wchar_t), &wsWin32PathLen) != S_OK) {
                // 乘法溢出,处理错误
                std::wcerr << L"Multiplication overflow occurred." << std::endl;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return E_FAIL;
            }

            strWin32Path = (PWSTR)CoTaskMemAlloc(wsWin32PathLen);

            if (strWin32Path == nullptr)
            {
                CoTaskMemFree(strPathPrefix);// 释放内存
                SetLastError(ERROR_NOT_ENOUGH_MEMORY);
                return E_FAIL;
            }

            nCoResponse = CoCreateSuffixFullPath(strWin32Path,
                nWin32Path, L"%s%s", strPathPrefix, &szKnownPath[0x26]);

            if (nCoResponse && nCoResponse == nWin32Path)
            {
                *szWin32FilePath = strWin32Path;
                CoTaskMemFree(strPathPrefix);// 释放内存
                return NOERROR;
            }

            CoTaskMemFree(strPathPrefix);// 释放内存
            return E_FAIL;
        }

        std::cerr << "SHGetKnownFolderPath Failed,errorCode = "
            << GetLastError() << std::endl;
        return str2GuidReslt;
    }
    std::cerr << "CLSIDFromString Failed,errorCode = "
        << GetLastError() << std::endl;
    return str2GuidReslt;
}

int CoCreateSuffixFullPath(
    wchar_t* wsBuffer,
    size_t wsCount,
    const wchar_t* wsFormat, ...
)
{
    if (wsBuffer == nullptr) return -1;

    int nWriteCount = 0;
    // hold the variable argument 
    va_list argsList = nullptr;

    // A function that invokes va_start 
    // shall also invoke va_end before it returns. 
    va_start(argsList, wsFormat);
    nWriteCount = vswprintf_s(wsBuffer, wsCount, wsFormat, argsList);
    va_end(argsList);

    return ++nWriteCount;
}



// wcslen 安全版本
size_t wcslen_s(
    const wchar_t* str, size_t ccmaxlen)
{
    size_t length = 0;

    if (ccmaxlen > 0x5000)
        ccmaxlen = 0x5000;  // 20480 字节,路径长度应该远小于该值

    __try {
        while (length < ccmaxlen && str[length] != L'\0') {
            ++length;
        }
        // 说明发生越界访问或者超出限制
        if (length == ccmaxlen)
        {
            std::cerr << "Trigger limit: The buffer exceeds the limit of characters." << std::endl;
            return 0;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        // 捕获并处理访问无效指针引发的异常
        std::cerr << "Access violation: Attempted to read from null pointer." << std::endl;
        return 0;
    }

    return length;
}


errno_t __fastcall memset_s(void* v, rsize_t smax, int c, rsize_t n) {
    if (v == NULL) return EINVAL;
    if (smax > RSIZE_MAX) return EINVAL;
    if (n > smax) return EINVAL;
    volatile unsigned char* p = (volatile unsigned char*)v;
    while (smax-- && n--) {
        *p++ = c;
    }
    return 0;
}


BYTE convertWcharToByte(wchar_t ch) {
    if (ch >= L'0' && ch <= L'9') {
        return static_cast<BYTE>(ch - L'0');
    }
    else {
        // 非数字字符
        return 255;
    }
}

测试截图:

SysTrayIconTool 帮助页面

此外,在 Win 11 更新 22621.1413 之前,我们对图标的管理也可以通过控制面板完成。

4.2 Win11 特有的转储信息

在注册表下,Win11 依赖设置来完成任务栏的通知区域的图标管理,在这里一切操作会立即生效,甚至不需要重启 explorer。

[HKEY_CURRENT_USER\Control Panel\NotifyIconSettings]

在该键下有很多以 TRAYNOTIFYICONID 命名的子键:

NotifyIconSettings 子键

每一个子键下包含 5 个值项:ExecutablePath、IconSnapshot、InitialTooltip、IsPromoted 和 UID。

例如:

管理通知图标配置注册表项

ExecutablePath 就是图标对应的可执行文件路径(已经对已知路径进行了 GUID 化处理);

IconSnapshot 是图标的一个快照,BitMap 数据结构;

InitialTooltip 是初始化时 lpData-> szTip 表示的文本(修改这个是没有效果的);

IsPromoted 是 bool 类型,表示是否显示在任务栏。如果该键的值为 1,则托盘图标始终可见,为 0 则表示隐藏。[该值是非常有用的,修改立即生效]

UID 是初始化时 lpData-> uID 表示的图标标识符,用于一定程度上规范使用并防止图标重复。

我已经在 Windows 11 中对此内容进行了测试。

值得注意的是,每个应用程序的密钥名称在每台计算机上都不一致,例如我检查的一台计算机上 OneDrive.exe 的密钥是:

HKEY_CURRENT_USER\Control Panel\NotifyIconSettings\5434357290411014857

但在另一个方面,同一个应用程序以不同的键名称列出:

HKEY_CURRENT_USER\Control Panel\NotifyIconSettings\5889842883606957982

此外还有可能出现同一个键名称对应多个不同程序的情况。

这些情况也会发生在同一设备上的不同用户之间。

因此,Windows 在为每个用户/系统命名密钥时似乎会生成一个唯一的应用程序 ID,但目前掌握的信息不足以让我们理解该 ID 生成的机制。因此,这里的解决方案是创建一个带有遍历循环的程序,搜索“NotifyIconSettings”中的每个键,以查找需要升级(或降级)的特定可执行文件,找到后,将 IsPromoted 表示的 DWORD 值编辑为 1(或 0)。

一个简易的测试代码如下:

/*
* ------------------------------------------------------------------------
* Visibility of the Application Icon if we use the Systray
* Version: 1.1
* 
* Works on Windows 11 Build 10.0.22621.1413 or greater
* Author: LianyiYou 516, at 2024/2/18. 
* ------------------------------------------------------------------------
*/

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

bool SetNotifyIconIsPromoted(HKEY TopKey, const std::wstring& sKeyName, int State, const std::wstring& ComputerName = L"")
{
    HKEY hKey = nullptr, lhRemoteRegistry = nullptr;
    DWORD r1 = 0, ret_value = 0;
    DWORD lpData = 0;
    DWORD lpcbData = sizeof(DWORD);
    DWORD lValue = 0;

    std::wstring trimmedKeyName = sKeyName;
    trimmedKeyName.erase(trimmedKeyName.find_last_not_of(L" \t") + 1);

    if (ComputerName.empty())
        r1 = RegOpenKeyEx(TopKey, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    else
    {
        r1 = RegConnectRegistry(ComputerName.c_str(), TopKey, &lhRemoteRegistry);
        if (r1 == ERROR_SUCCESS)
            r1 = RegOpenKeyEx(lhRemoteRegistry, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    }

    if (r1 == ERROR_SUCCESS)
    {
        if (State == 0 || State == 1)
            r1 = RegSetValueEx(hKey, L"IsPromoted", 0, REG_DWORD, reinterpret_cast<BYTE*>(&State), sizeof(DWORD));

        if (r1 == ERROR_SUCCESS)
            ret_value = true;
    }

    RegCloseKey(hKey);

    if (lhRemoteRegistry)
        RegCloseKey(lhRemoteRegistry);

    return ret_value;
}

int GetNotifyIconIsPromoted(HKEY TopKey, const std::wstring& sKeyName, const std::wstring& ComputerName = L"")
{
    int ret_value = 0;
    HKEY hKey = nullptr, lhRemoteRegistry = nullptr;
    DWORD lpData = 0;
    DWORD lpcbData = sizeof(DWORD);
    DWORD lType = 0;
    DWORD lpDataDWORD = 0;
    DWORD r1 = 0;

    std::wstring trimmedKeyName = sKeyName;
    trimmedKeyName.erase(trimmedKeyName.find_last_not_of(L" \t") + 1);

    if (ComputerName.empty())
        r1 = RegOpenKeyEx(TopKey, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    else
    {
        r1 = RegConnectRegistry(ComputerName.c_str(), TopKey, &lhRemoteRegistry);
        if (r1 == ERROR_SUCCESS)
            r1 = RegOpenKeyEx(lhRemoteRegistry, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    }

    if (r1 == ERROR_SUCCESS)
    {
        r1 = RegQueryValueEx(hKey, L"IsPromoted", 0, &lType, reinterpret_cast<BYTE*>(&lpDataDWORD), &lpcbData);
        if (r1 == ERROR_SUCCESS && lType == REG_DWORD)
        {
            ret_value = static_cast<int>(lpDataDWORD);
            //std::cout << "IsPromoted == " << ret_value << std::endl;
        }

        RegCloseKey(hKey);
    }

    if (lhRemoteRegistry)
        RegCloseKey(lhRemoteRegistry);

    return ret_value;
}

#define RSIZE_MAX (SIZE_MAX >> 1)

errno_t WINAPI memset_s(void* v, rsize_t smax, int c, rsize_t n) {
    if (v == NULL) return EINVAL;
    if (smax > RSIZE_MAX) return EINVAL;
    if (n > smax) return EINVAL;
    volatile unsigned char* p = (volatile unsigned char*)v;
    while (smax-- && n--) {
        *p++ = c;
    }
    return 0;
}

std::wstring GetKeyNotifyIcon(
    HKEY TopKey, 
    const std::wstring& sKeyName, 
    const std::wstring& sProgramFileName, 
    const std::wstring& ComputerName = L""
)
{
    HKEY hKey = nullptr, lhRemoteRegistry = nullptr;
    DWORD r1 = 0, index = 0, lpcbData = 0, lType = 0;
    FILETIME lpftLastWriteTime = { 0 };
    wchar_t lpData[1024] = { 0 };
    std::wstring subkey, listsubkey;
    std::vector<std::wstring> listsubkeys;

    std::wstring trimmedKeyName = sKeyName;
    trimmedKeyName.erase(trimmedKeyName.find_last_not_of(L" \t") + 1);

    if (ComputerName.empty())
        r1 = RegOpenKeyEx(TopKey, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    else
    {
        r1 = RegConnectRegistry(ComputerName.c_str(), TopKey, &lhRemoteRegistry);
        if (r1 == ERROR_SUCCESS)
            r1 = RegOpenKeyEx(lhRemoteRegistry, trimmedKeyName.c_str(), 0, KEY_ALL_ACCESS, &hKey);
    }

    if (r1 == ERROR_SUCCESS)
    {
        for (index = 0; index < 1000; index++)
        {
            lpcbData = 1023;
            r1 = RegEnumKeyEx(hKey, index, lpData, &lpcbData, 0, 0, 0, &lpftLastWriteTime);
            if (r1 == ERROR_SUCCESS && lpcbData)
            {
                lpData[lpcbData] = 0;
                listsubkeys.push_back(std::wstring(lpData, lpcbData));
            }
            else
                break;
        }

        RegCloseKey(hKey);

        for (std::wstring& subkey : listsubkeys)
        {
            std::wstring keyPath = trimmedKeyName + L"\\" + subkey;
            if (ComputerName.empty())
                r1 = RegOpenKeyEx(TopKey, keyPath.c_str(), 0, KEY_ALL_ACCESS, &hKey);
            else
            {
                r1 = RegConnectRegistry(ComputerName.c_str(), TopKey, &lhRemoteRegistry);
                if (r1 == ERROR_SUCCESS)
                    r1 = RegOpenKeyEx(lhRemoteRegistry, keyPath.c_str(), 0, KEY_ALL_ACCESS, &hKey);
            }

            if (r1 == ERROR_SUCCESS)
            {
                lpcbData = 1023;
                memset_s(lpData, sizeof(lpData), 0, sizeof(lpData));
                r1 = RegQueryValueEx(hKey, L"ExecutablePath", 0, &lType, reinterpret_cast<BYTE*>(&lpData), &lpcbData);
                if (r1 == ERROR_SUCCESS && lType == REG_SZ)
                {
                    std::wstring executablePath(lpData, lpcbData);
                    size_t nPos = executablePath.find(sProgramFileName);
                    if (nPos != std::string::npos)
                    {
                        listsubkey = subkey;
                        break;
                    }
                }

                RegCloseKey(hKey);
            }

            if (lhRemoteRegistry)
                RegCloseKey(lhRemoteRegistry);
        }
    }

    return listsubkey;
}

int main()
{
    // Set the code page to support Chinese input and output
    _wsetlocale(LC_ALL, L".UTF8");
    HKEY topKey = HKEY_CURRENT_USER;
    std::wstring keyName = L"Control Panel\\NotifyIconSettings";
    std::wstring programFileName = L"Taskmgr.exe";

    // Find named TrayIconID
    std::wstring subkey = GetKeyNotifyIcon(topKey, keyName, programFileName);
    if (!subkey.empty())
    {
        // Get current IsPromoted value
        int isPromoted = GetNotifyIconIsPromoted(topKey, keyName + L"\\" + subkey);
        std::wstring registerPath = L"HKEY_CURRENT_USER\\" + keyName + L"\\" + subkey;
        std::wcout << "registerPath == " << registerPath << std::endl;
        std::wcout << "IsPromoted == " << isPromoted << std::endl;

        // Set IsPromoted to 1
        SetNotifyIconIsPromoted(topKey, keyName + L"\\" + subkey, 1);

        // Get the updated IsPromoted value
        isPromoted = GetNotifyIconIsPromoted(topKey, keyName + L"\\" + subkey);
        std::wcout << "IsPromoted == " << isPromoted << std::endl;
    }

    return 0;
}
Windows 11 实测图(源代码允许 taskmgr.exe 始终显示)

【完】

P.S.:本文讨论了在 Win11 22H2 上获取系统托盘图标信息的方法,即拦截 Shell_TrayWnd 窗口的 WM_COPYDATA 消息。我主要使用 Inline hook 重写 CTray::v_WndProc 函数,也就是窗口过程函数来拦截 WM_COPYDATA 消息。

具体分析了两种注入 explorer 的实现方法:

(1)创建挂起进程的远程线程注入;

(2)模拟调试进程的 TLS 函数注入。

事实上,挂钩 CTray::v_WndProc 函数并不是开销最小的方法。在第 4 篇文章中,我将分析通过 SetWindowsHookEx 实现消息钩子的角度分析如何拦截 WM_COPYDATA,该方法与拦截未导出的 CTray::v_WndProc 函数相比,将更加易于实现。

【更新日志】

2024.06.01:修正代码中使用 std::cout 导致在早于 Win 10 的系统上运行存在中文乱码问题,改用 C 语言风格的输出,暂未找到 std::cout 乱码原因。


本文属于原创文章,转载请注明出处:

https://blog.csdn.net/qq_59075481/article/details/136134195

本文发布于:2024.02.20,更新于:2024.03.22,2024.04.13 ,2024.05.24,2024.06.01。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

涟幽516

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

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

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

打赏作者

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

抵扣说明:

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

余额充值