windows實時監測熱插拔裝置的變化
序:
在21世紀,這個資訊時代,熱插拔裝置是一個巨大的安全隱患。在這個篇文章中,我將介紹一種在使用者模式下檢測即插即用裝置的方法。比如,在系統中插入一個usb裝置,ipod,無線網絡卡等等,都可以在使用者模式下檢測到,並決定開啟或關閉新插入的裝置。並且,在文章結尾,我將介紹一下這種方法的優點,以及限制。
怎樣檢測硬體改變呢?
事實上,windows作業系統在檢測到硬體變化時,會發送一個WM_DEVICECHANGE硬體change訊息。因此,我們要做的就是在我們的程式中新增WM_DEVICECHANGE的訊息響應。
程式碼事例如下:
BEGIN_MESSAGE_MAP(CHWDetectDlg, CDialog)zai // ... other handlers ON_MESSAGE(WM_DEVICECHANGE, OnMyDeviceChange) END_MESSAGE_MAP() LRESULT CHWDetectDlg::OnMyDeviceChange(WPARAM wParam, LPARAM lParam) { // for more information, see MSDN help of WM_DEVICECHANGE // this part should not be very difficult to understand if ( DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam ) { PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam; switch( pHdr->dbch_devicetype ) { case DBT_DEVTYP_DEVICEINTERFACE: PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr; // do something... break; case DBT_DEVTYP_HANDLE: PDEV_BROADCAST_HANDLE pDevHnd = (PDEV_BROADCAST_HANDLE)pHdr; // do something... break; case DBT_DEVTYP_OEM: PDEV_BROADCAST_OEM pDevOem = (PDEV_BROADCAST_OEM)pHdr; // do something... break; case DBT_DEVTYP_PORT: PDEV_BROADCAST_PORT pDevPort = (PDEV_BROADCAST_PORT)pHdr; // do something... break; case DBT_DEVTYP_VOLUME: PDEV_BROADCAST_VOLUME pDevVolume = (PDEV_BROADCAST_VOLUME)pHdr; // do something... break; } } return 0; }
然而預設情況下,Windows作業系統傳送WM_DEVICECHANGE有些限制:
1 只有頂層窗體的程式才能收到這個訊息
2 僅僅串列埠、磁碟發生改變,才對每個程式廣播這個訊息
的確不錯,至少你可以知道移動U盤、行動硬碟、光碟被安裝或彈出了,通過DEV_BROADCAST_VOLUME.dbcv_unitmask你也可以獲得其對應的碟符。但實際上,你不知道底層處理的是哪個物理裝置實際上被安裝到了系統中。
API:RegisterDeviceNotification()
所以,你不得不呼叫RegisterDeviceNotification()API來註冊其他型別的裝置改變,或是你的程式僅僅是一個服務程式、沒有頂層窗體的程式。例如:如下的例子是用來註冊一個裝置型別的介面的:
DEV_BROADCAST_DEVICEINTERFACE NotificationFilter; ZeroMemory( &NotificationFilter, sizeof(NotificationFilter) ); NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; // assume we want to be notified with USBSTOR // to get notified with all interface on XP or above // ORed 3rd param with DEVICE_NOTIFY_ALL_INTERFACE_CLASSES and dbcc_classguid will be ignored NotificationFilter.dbcc_classguid = GUID_DEVINTERFACE_USBSTOR; HDEVNOTIFY hDevNotify = RegisterDeviceNotification(this->GetSafeHwnd(), amp;NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE); if( !hDevNotify ) { // error handling... return FALSE; }
請注意第8行,NotificationFilter.dbcc_classguid
關注的就是你關心的一類裝置。
一個支援即插即用的裝置,有2個不同的GUID相關,一個裝置介面GUID, 一個是裝置類GUID
裝置類GUID:定義了廣泛意義上一類裝置的GUID,如果你開啟裝置管理器[我的電腦右鍵—>裝置管理器],預設的是按照“型別”排列的,每一個“型別”就是一個裝置類,同時每一個裝置類有一個唯一的ID就是裝置類GUID。裝置GUID定義了此類裝置的圖示、預設的安全設定、安裝屬性(例如使用者不能手動安裝這類裝置,而必須通過PNP來遍歷),以及其他的設定資訊。裝置類GUID沒有定義對應的I/O介面(請參考術語表),而更像是裝置的分組。我認為一個比較好的例子是埠類。串列埠COM和並口LPT 都是埠類的一部分,但其各有各的I/O介面,而且彼此互不相容.一個裝置僅僅屬於一個裝置類。我們可以通過裝置驅動的INF檔案的開頭來檢視該裝置的裝置類GUID。
裝置介面GUID:定義了相互關聯I/O介面的GUID,每一個介面GUID的具體例項都支援基本的I/O設定。裝置介面GUID也是對應的驅動程式基於PNP狀態來註冊、啟用、禁用裝置。如果需要,一個裝置甚至可以註冊多個同樣GUID的例項(假使每個都有相同的名字)[注:在實際的程式中,多次插拔USB口,確實會驅出相同的串列埠,例如port12,port12,port12…],儘管在現實世界中完全不需要這樣。一個簡單的I/O關聯介面是鍵盤裝置,每個鍵盤裝置的介面GUID必須相同。
可以通過如下的登錄檔路徑來檢視當前裝置類GUID, 裝置介面GUID:
- \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class
- \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses
常用裝置的介面GUID如下:
Device Interface Name | GUID |
USB Raw Device | {a5dcbf10-6530-11d2-901f-00c04fb951ed} |
Disk Device | {53f56307-b6bf-11d0-94f2-00a0c91efb8b} |
Network Card | {ad498944-762f-11d0-8dcb-00c04fc3358c} |
Human Interface Device (HID) | {4d1e55b2-f16f-11cf-88cb-001111000030} |
Palm | {784126bf-4190-11d4-b5c2-00c04f687a67} |
解析DEV_BROADCAST_DEVICEINTERFACE
如下是修改處理捕獲對應事件的函式:
LRESULT CHWDetectDlg::OnMyDeviceChange(WPARAM wParam, LPARAM lParam)
{
....
....
if ( DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam )
{
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
switch( pHdr->dbch_devicetype )
{
case DBT_DEVTYP_DEVICEINTERFACE:
PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
UpdateDevice(pDevInf, wParam);
break;
....
....
}
從MSDN中,我們知道
typedef struct _DEV_BROADCAST_DEVICEINTERFACE {
DWORD dbcc_size;
DWORD dbcc_devicetype;
DWORD dbcc_reserved;
GUID dbcc_classguid;
TCHAR dbcc_name[1];
} DEV_BROADCAST_DEVICEINTERFACE *PDEV_BROADCAST_DEVICEINTERFACE;
我們似乎可以通過dbcc_name知道那個裝置安裝到了當前系統。答案是錯誤的,dbcc_name僅僅是作業系統內部使用來做為ID的,其實不易讀的,例如下面的這個dbcc_name:
\\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
- \\?\USB: USB 意思是這是一個USB裝置類
- Vid_04e8&Pid_053b:
Vid
/Pid
是硬體ID,由廠商ID和產品ID組成(但這是由裝置類指定的,USB裝置類使用VID/PID,不同的裝置類使用不同的命名約定) - 002F9A9828E0F06: 不清楚是怎麼生成的,是唯一裝置ID
- {a5dcbf10-6530-11d2-901f-00c04fb951ed}:裝置介面類GUID
現在,我們來解出裝置描述資訊或是裝置別名,有2種辦法:
1 直接讀登錄檔, \\HKLM\SYSTEM\CurrentControlSet\Enum\USB\Vid_04e8&Pid_503b\0002F9A9828E0F06
2 使用 SetupDiXxx 系列API
API:SetupDiXxx()
Windows定義了一組API,讓使用者通過程式設計的辦法來獲取對應的硬體裝置資訊。例如,我們可以通過dbcc_name來獲得裝置描述資訊或是裝置別名。下面是這個辦法都具體步驟:
1 首先通過SetupDiGetClassDevs()來獲得裝置資訊集 HDEVINFO,這個操作等同於是一個獲取目錄控制代碼的過程。
2 接著使用SetupDiEnumDeviceInfo()來遍歷出這個裝置資訊集內的所有裝置,這個操作等同於把目錄列表的過程。對於每個遍歷出的,我們可以獲得SP_DEVINFO_DATA,這個等同於是檔案控制代碼。
3 在上面的列舉過程中,使用SetupDiGetDeviceInstanceId()來讀取每個裝置的例項ID,這個操作等同於是讀檔案的屬性,一個裝置的例項ID類似這個:”USB\Vid_04e8&Pid_503b\0002F9A9828E0F06”,和dbcc_name非常像。
4 如果裝置的例項ID等同於dbcc_name,則通過SetupDiGetDeviceRegistryProperty()來獲取裝置描述資訊或是裝置別名資訊。
程式如下:
void CHWDetectDlg::UpdateDevice(PDEV_BROADCAST_DEVICEINTERFACE pDevInf, WPARAM wParam)
{
// dbcc_name:
// \\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
// convert to
// USB\Vid_04e8&Pid_503b\0002F9A9828E0F06
ASSERT(lstrlen(pDevInf->dbcc_name) > 4);
CString szDevId = pDevInf->dbcc_name+4;
int idx = szDevId.ReverseFind(_T('#'));
ASSERT( -1 != idx );
szDevId.Truncate(idx);
szDevId.Replace(_T('#'), _T('\\'));
szDevId.MakeUpper();
CString szClass;
idx = szDevId.Find(_T('\\'));
ASSERT(-1 != idx );
szClass = szDevId.Left(idx);
// if we are adding device, we only need present devices
// otherwise, we need all devices
DWORD dwFlag = DBT_DEVICEARRIVAL != wParam
? DIGCF_ALLCLASSES : (DIGCF_ALLCLASSES | DIGCF_PRESENT);
HDEVINFO hDevInfo = SetupDiGetClassDevs(NULL, szClass, NULL, dwFlag);
if( INVALID_HANDLE_VALUE == hDevInfo )
{
AfxMessageBox(CString("SetupDiGetClassDevs(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
return;
}
SP_DEVINFO_DATA* pspDevInfoData =
(SP_DEVINFO_DATA*)HeapAlloc(GetProcessHeap(), 0, sizeof(SP_DEVINFO_DATA));
pspDevInfoData->cbSize = sizeof(SP_DEVINFO_DATA);
for(int i=0; SetupDiEnumDeviceInfo(hDevInfo,i,pspDevInfoData); i++)
{
DWORD DataT ;
DWORD nSize=0 ;
TCHAR buf[MAX_PATH];
if ( !SetupDiGetDeviceInstanceId(hDevInfo, pspDevInfoData, buf, sizeof(buf), &nSize) )
{
AfxMessageBox(CString("SetupDiGetDeviceInstanceId(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
break;
}
if ( szDevId == buf )
{
// device found
if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_FRIENDLYNAME, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
// do nothing
} else if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_DEVICEDESC, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
// do nothing
} else {
lstrcpy(buf, _T("Unknown"));
}
// update UI
// .....
// .....
break;
}
}
if ( pspDevInfoData ) HeapFree(GetProcessHeap(), 0, pspDevInfoData);
SetupDiDestroyDeviceInfoList(hDevInfo);
}
文章來自:http://blog.csdn.net/windows_nt
禁用裝置
假使你有一個正確的HDEVINFO和SP_DEVINFO_DATA(實際上,我們保持dbcc_name座位樹節點的tag,當右鍵單擊某一個節點的時候,可以通過呼叫SetupDiGetClassDevs和SetupDiEnumDeviceInfo來獲得所需東西),按照如下的步驟即可禁用一個裝置:
1 給SP_PROPCHANGE_PARAMS結構體賦上正確的值
2 把上面賦完值的SP_PROPCHANGE_PARAMS作為引數傳入到SetupDiSetClassInstallParams()
3 呼叫SetupDiCallClassInstaller(),傳遞引數DIF_PROPEFRTYCHANGE
實際上,DIF也是按位做與運算後相容的,你也可以去傳遞不同的DIF引數來呼叫SetupDiSetClassInstallParams()。 更多資訊,請參考MSDN”Handling DIF Codes”
SP_PROPCHANGE_PARAMS spPropChangeParams ;
spPropChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
spPropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE ;
spPropChangeParams.Scope = DICS_FLAG_GLOBAL ;
spPropChangeParams.HwProfile = 0; // current hardware profile
spPropChangeParams.StateChange = DICS_DISABLE
if( !SetupDiSetClassInstallParams(hDevInfo, &spDevInfoData,
// note we pass spPropChangeParams as SP_CLASSINSTALL_HEADER
// but set the size as sizeof(SP_PROPCHANGE_PARAMS)
(SP_CLASSINSTALL_HEADER*)&spPropChangeParams, sizeof(SP_PROPCHANGE_PARAMS)) )
{
// handle error
}
else if(!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfo, &spDevInfoData))
{
// handle error
}
else
{
// ok, show disable success dialog
// note, after that, the OS will post DBT_DEVICEREMOVECOMPLETE for the disabled device
}
附錄:
我使用這個程式,已經多次測試了USB的無線網絡卡的,插入、拔出測試。
侷限性:
1 明顯的,必須先執行該程式,才能檢測硬體裝置。例如:裝置在作業系統啟動前就已經連線,或者在這個程式執行前的連線都不會被檢測。但這個問題,可以通過儲存當前系統配置到遠端計算機上,等啟動完這個程式後再堅持不同的配置來解決
2 我們可以禁用裝置,換而言之這也是我們所有能做到。我們不能訪問裝置底層控制。 我認為可以通過重新用基於核心的過濾驅動來實現,則可以解決這個問題。
非常好的文章