0%

在服务中监控U盘接入并获取盘符(1)

简介

在窗口程序中,我们可以通过 WM_DEVICECHANGE 消息获取到接入的U盘设备,
某些情况下,我们需要在服务中去监控U盘设备接入,并获取对应盘符,来进行其他操作。

工作原理

创建服务的本身框架,这里不再复述,主要介绍如何监控U盘相关。
在使用 StartServiceCtrlDispatcher 注册的服务入口函数 ServiceMain 中,我们需要使用
RegisterServiceCtrlHandlerEx 注册服务的 HandlerProc 消息处理函数,然后使用注册后
得到的 SERVICE_STATUS_HANDLE 句柄,来注册U盘设备监控的通知函数

1
2
3
4
5
#include <windows.h>
#include <winioctl.h>
#include <dbt.h>
SERVICE_STATUS_HANDLE g_hServiceStatus = NULL;
HDEVNOTIFY g_hDevNotify = NULL;
1.服务入口函数

注册监控时,我们只能使用 DBT_DEVTYP_DEVICEINTERFACEDBT_DEVTYP_HANDLE 类型,
其中 DBT_DEVTYP_HANDLE 是针对某个设备的各种IO行为的监控,不符合我们的预期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VOID WINAPI ServiceMain(DWORD argc, LPWSTR *argv)
{
// 注册服务的消息处理函数
g_hServiceStatus = RegisterServiceCtrlHandlerExW(L"MyService", &HandlerProc, NULL);
if (!g_hServiceStatus) return;
// 监控GUID_DEVINTERFACE_VOLUME设备
DEV_BROADCAST_DEVICEINTERFACE stDbdi = { 0 };
stDbdi.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
stDbdi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
stDbdi.dbcc_classguid = GUID_DEVINTERFACE_VOLUME;
// 注册设备监控通知函数
g_hDevNotify = RegisterDeviceNotificationW(
g_hServiceStatus, &stDbdi, DEVICE_NOTIFY_SERVICE_HANDLE);
if (!g_hDevNotify) return;
// 设置为运行状态
ChangeServiceStatus(SERVICE_RUNNING);
}
2.服务消息处理函数

在消息处理函数中,当Volume设备接入时,会触发 SERVICE_CONTROL_DEVICEEVENT 消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
DWORD WINAPI HandlerProc(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
switch (dwControl)
{
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
// 取消注册设备监控通知函数
UnregisterDeviceNotification(g_hDevNotify);
ChangeServiceStatus(SERVICE_STOPPED);
break;

case SERVICE_CONTROL_DEVICEEVENT:
if (!lpEventData) break;
switch (dwEventType)
{
case DBT_DEVICEARRIVAL: // 设备接入
{
PDEV_BROADCAST_HDR pDbch = (PDEV_BROADCAST_HDR)lpEventData;
if (pDbch->dbch_devicetype != DBT_DEVTYP_DEVICEINTERFACE) break;
// 根据Volume获取对应盘符
GetVolumeDriveName(((PDEV_BROADCAST_DEVICEINTERFACE)pDbch)->dbcc_name);
}
break;

case DBT_DEVICEREMOVECOMPLETE: // 设备移除
break;
}
break;
}
return NO_ERROR;
}
3.根据Volume获取盘符

监控到U盘接入时,获取到的设备是 DBT_DEVTYP_DEVICEINTERFACE 类型的,而不是在窗口中
DBT_DEVTYP_VOLUME 类型,而 DEV_BROADCAST_DEVICEINTERFACEdbcc_name 成员是
\\?\STORAGE#Volume#_??_USBSTOR#Disk&Ven_Kingston&Prod_DataTraveler_3.0 ...
的样式,通过使用 WinObj 工具查询,发现这个名称对应到了 \Device\HarddiskVolume???
的符号链接,我们知道盘符其实也是对应到这样的符号链接上,所以我们可以使用 ntdll.dll
导出的 NtOpenSymbolicLinkObjectNtQuerySymbolicLinkObject 函数来查询符号链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
WCHAR GetVolumeDriveName(PCWSTR pDevName)
{
if (!pDevName || !pDevName[0]) return 0;
WCHAR wszBuffer[512] = { 0 };
wcscpy_s(wszBuffer, pDevName);
// 把前缀"\\\\?\\"修改为"\\??\\"
if (wcsncmp(wszBuffer, L"\\\\?\\", 4)) return 0;
wszBuffer[1] = L'?';
// 获取ntdll.dll的导出函数
HMODULE hNtdll = LoadLibraryW(L"ntdll.dll");
if (!hNtdll) return 0;
NTOPENSYMBOLICLINKOBJECT fnNtOpenSymbolicLinkObject =
(NTOPENSYMBOLICLINKOBJECT)GetProcAddress(hNtdll, "NtOpenSymbolicLinkObject");
NTQUERYSYMBOLICLINKOBJECT fnNtQuerySymbolicLinkObject =
(NTQUERYSYMBOLICLINKOBJECT)GetProcAddress(hNtdll, "NtQuerySymbolicLinkObject");
NTCLOSE fnNtClose = (NTCLOSE)GetProcAddress(hNtdll, "NtClose");
if (!fnNtOpenSymbolicLinkObject || !fnNtQuerySymbolicLinkObject || !fnNtClose)
{
FreeLibrary(hNtdll);
return 0;
}
// 初始化UNICODE_STRING信息
UNICODE_STRING strDevName = { 0 };
OBJECT_ATTRIBUTES objAttrib = { 0 };
strDevName.Buffer = wszBuffer;
strDevName.MaximumLength = sizeof(wszBuffer);
strDevName.Length = (USHORT)wcslen(wszBuffer) * sizeof(WCHAR);
InitializeObjectAttributes(&objAttrib, &strDevName, OBJ_CASE_INSENSITIVE, NULL, NULL);
// 打开Volume设备
HANDLE hSymLink = NULL;
NTSTATUS ntStatus = fnNtOpenSymbolicLinkObject(&hSymLink, SYMBOLIC_LINK_QUERY, &objAttrib);
if (ntStatus != STATUS_SUCCESS)
{
FreeLibrary(hNtdll);
return 0;
}
// 查询符号链接
ULONG dwLength = 0;
strDevName.Length = 0;
ZeroMemory(wszBuffer, sizeof(wszBuffer));
ntStatus = fnNtQuerySymbolicLinkObject(hSymLink, &strDevName, &dwLength);
fnNtClose(hSymLink); // 关闭句柄
FreeLibrary(hNtdll); // 关闭句柄
if (ntStatus != STATUS_SUCCESS) return 0;
// 查询现有的盘符和对应的Volume信息
WCHAR wszDrive[128] = { 0 }; // 32 * L"C:\\"
WCHAR wszVolume[32] = { 0 }; // L"\\Device\\HarddiskVolume?????"
DWORD dwNum = GetLogicalDriveStringsW(128, wszDrive);
for (DWORD i = 0; i < dwNum; i += 4)
{
wszDrive[i + 2] = 0; // 去掉'\\'只保留"C:"
// 检测当前盘符是不是U盘
if (GetDriveType(&wszDrive[i]) != DRIVE_REMOVABLE) continue;
if (!QueryDosDeviceW(&wszDrive[i], wszVolume, 32)) continue;
// 当Volume信息匹配时返回当前盘符
if (!_wcsicmp(wszVolume, wszBuffer)) return wszDrive[i];
}
return 0;
}