Безопасное извлечение USB-устройств — Архив WASM.RU

Все статьи

Безопасное извлечение USB-устройств — Архив WASM.RU

Цель данной статьи - описание метода безопасного извлечения USB-устройств. В данном случаи под USB-устройствами будут, подразумевается в основном устройства хранения информации (Flash, HDD), но все описанные ниже действия применимы и устройствам других классов. Существует мнение, что данная функция не нужна при обращении к USB-устройствам. Обычно нет ничего страшного в том, что устройство было извлечено небезопасно. В крайнем случае, может потеряться информация, которая еще не была записана на USB (например, если у вас включено кэширование записи на диск). Но все же осторожность не повредит. Следующий вопрос, который может возникнуть, это зачем писать то, что уже реализовано в Windows. Например, быстро вызвать окно безопасного отключения устройств, можно выполнив команду:

rundll32.exe shell32.dll,Control_RunDLL hotplug.dll

Здесь можно заметить, что реализация Microsoft не всем нравится. Тем более что можно развить мысль, например, вести поиск процессов, из-за которых невозможно безопасное извлечение. Т.е. процессов, которые имеют незакрытые дескрипторы файлов или папок, расположенных на съемном носителе.

Итак, начнем с того, что построим список всех устройств компьютера, с указанием, какие из них поддерживают безопасное извлечение. Все устройства в компьютере связаны “родственными узами”, т.е. могут быть представлены узлами некого дерева. Это выглядит примерно так:

HTREE\ROOT\0
  ROOT\ACPI_HAL\0000 'Однопроцессорный компьютер с ACPI'
    ACPI_HAL\PNP0C08\0 'Microsoft ACPI-совместимая система'
      ACPI\GENUINEINTEL_-_X86_FAMILY_15_MODEL_2\_0 'Intel(R) Celeron(R) CPU 2.40GHz'
      ACPI\PNP0A03\0 'Шина PCI'
        PCI\VEN_8086&DEV_2560&SUBSYS_0000&REV_03\3&267A616A&0&00 'Intel(R) 82845G/GL/GE/PE/GV Processor to I/O Controller - 2560 '
        PCI\VEN_8086&DEV_2561&SUBSYS_0000&REV_03\3&267A616A&0&08 'Intel(R) 82845G/GL/GE/PE/GV Processor to AGP Controller - 2561 '
          PCI\VEN_10DE&DEV_0181&SUBSYS_31021458&REV_C1\4&BCE2C57&0&0008 'NVIDIA GeForce4 MX 440 with AGP8X'
            DISPLAY\SAM01F9\5&EB43F14&1&22446688&01&00 'Модуль подключения монитора' [REMOVEABLE]
			PCI\VEN_8086&DEV_24C2&SUBSYS_53568086&REV_02\3&267A616A&0&E8 'Intel(R) 82801DB/DBM USB Universal Host Controller - 24C2 '
            USB\ROOT_HUB20\4&3B39A3A1&0 'Корневой USB концентратор'
			USB\VID_04CF&PID_8818\100 'Запоминающее устройство для USB' [REMOVEABLE]
              USBSTOR\DISK&VEN_SAMSUNG&PROD_MP0402H&REV_UC10\100&0 'SAMSUNG MP0402H USB Device'

Существуют несколько способов обхода графа (двоичного дерева) – рекурсивный и нерекурсивный методы [1],[2]. Для простоты применим рекурсивный метод.

Необходимые нам функции относятся к группе “PnP Configuration Manager Functions”, имеют префикс “CM_” и экспортируются библиотекой setupapi.dll.

Для использования этой библиотеки в MASM32 необходимо подключить setupapi.inc. Описание можно получить в MSDN, но на всякий случай я приведу здесь краткое описание основных используемых функций.

CM_Locate_DevNode – используется нами для получения корневого узла дерева устройств.

CM_Locate_DevNode(
    OUT PDEVINST	pdnDevInst,
    IN DEVINSTID	pDeviceID,  OPTIONAL
    IN ULONG		ulFlags
    );

Параметры

  • pdnDevInst – указатель на переменную, куда будет записан дескриптор устройства
  • pDeviceID – указатель на строку с идентификатором устройства. Если ноль, то возвращается корневой узел дерева устройств
  • ulFlags – обычно функцию вызывают с флагом CM_LOCATE_DEVNODE_NORMAL
CM_Get_DevNode_Status – позволяет получить статус устройства по которому можно определить, можно ли извлечь данное устройство. Если в статусе флаг DN_REMOVABLE установлен, то устройство можно извлечь.

CM_Get_DevNode_Status(
    OUT PULONG		pulStatus,
    OUT PULONG		pulProblemNumber,
    IN DEVINST		dnDevInst,
    IN ULONG		ulFlags
    );

Параметры

  • pulStatus – указатель на переменную типа DWORD со статусом устройства
  • pulProblemNumber – указатель на переменную типа DWORD с номером ошибки
  • dnDevInst – идентификатор устройства
  • ulFlags – не используется. Должен быть нулем

CM_Get_Device_ID_Size – выдает размер строки идентификатора устройства

CM_Get_Device_ID_Size(
    OUT PULONG		pulLen,
    IN DEVINST		dnDevInst,
    IN ULONG		ulFlags
    );

Параметры

  • pulLen – указатель на переменную для записи длины строки
  • dnDevInst – идентификатор устройства
  • ulFlags – не используется. Должен быть нулем
CM_Get_Device_ID – позволяет узнать идентификатор устройства.

CM_Get_Device_ID(
    IN DEVINST		dnDevInst,
    OUT PTCHAR		Buffer,
    IN ULONG		BufferLen,
    IN ULONG		ulFlags
    );

Параметры

  • dnDevInst – идентификатор устройства
  • Buffer – указатель на буфер для записи строки идентификатора устройства
  • BufferLen – длина строки идентификатора устройства
  • ulFlags – не используется. Должен быть нулем

CM_Get_Child – необходима для получения потомка данного узла

CM_Get_Child(
    OUT PDEVINST	pdnDevInst,
    IN DEVINST		dnDevInst,
    IN ULONG		ulFlags
    );

Параметры

  • pdnDevInst – указатель на идентификатор устройства потомка
  • dnDevInst – идентификатор устройства
  • ulFlags – не используется. Должен быть нулем

CM_Get_Sibling – необходима для получения узла, находящегося следом за данным

CM_Get_Sibling(
    OUT PDEVINST	pdnDevInst,
    IN DEVINST		DevInst,
    IN ULONG		ulFlags
    );

Параметры

  • pdnDevInst – указатель на идентификатор смежного устройства
  • dnDevInst – идентификатор устройства
  • ulFlags – не используется. Должен быть нулем

CM_Locate_DevNode – позволяет получить дескриптор устройства по строке идентификатору

CM_Locate_DevNode (
    OUT PDEVINST	pdnDevInst,
    IN DEVINSTID	pDeviceID,  OPTIONAL
    IN ULONG		ulFlags,
    );

Параметры

  • pdnDevInst – указатель на дескриптор устройства
  • pDeviceID – указатель на строку идентификатор устройства
  • ulFlags – флаг мы будем использовать CM_LOCATE_DEVNODE_NORMAL

CM_Request_Device_Eject – как раз эта функция и позволяет безопасно извлечь устройство

CM_Request_Device_Eject(
    IN DEVINST			dnDevInst,
    OUT PPNP_VETO_TYPE	pVetoType,
    OUT LPTSTR			pszVetoName,
    IN ULONG			ulNameLength,
    IN ULONG			ulFlags
    );

Параметры

  • dnDevInst – идентификатор устройства
  • pVetoType – в данном случае не нужно
  • pszVetoName – в данном случае не нужно
  • ulNameLength – в данном случае не нужно
  • ulFlags – не используется. Должен быть нулем

CM_Get_DevNode_Registry_Property – извлекает спец. информацию об устройстве из соответствующих ключей реестра.

CM_Get_DevNode_Registry_Property(
    IN DEVINST		dnDevInst,
    IN ULONG		ulProperty,
    OUT PULONG		pulRegDataType, OPTIONAL
    OUT PVOID		Buffer, OPTIONAL
    IN OUT PULONG	pulLength,
    IN ULONG		ulFlags
    );

Параметры

  • dnDevInst – идентификатор устройства
  • ulProperty – константа с префиксом CM_DRP_ которая идентифицирует, какое из свойств устройство необходимо. Все эти константы объявлены в файле cfgmgr32.h.
  • pulRegDataType – не обязательный параметр может быть NULL. Указатель на переменную, содержащую тип запрашиваемого значения.
  • Buffer – адрес буфера для возвращаемого значения. Если этот параметр NULL, то функция вернет размер возвращаемой строки
  • pulLength – указатель на переменную для записи размера полученной строки.
  • ulFlags – не используется. Должен быть нулем

Теперь приступаем непосредственно к коду. Ниже приводится фрагмент, отвечающий за обход дерева устройств. Что бы не загромождать код, устройства будем перечислять в виде списка.

EnumDevice proc	dDevInst:DWORD

LOCAL	NewDevInst	:DWORD
LOCAL	len		:DWORD
	;Получение статуса устройства
	invoke	CM_Get_DevNode_Status,addr Status,addr ProblemNum,dDevInst,0
	;Получение идентификатора устройства
	invoke	CM_Get_Device_ID_Size,addr DevLen,dDevInst,0
	inc		DevLen
	invoke	CM_Get_Device_ID,dDevInst,addr aDeviceId,DevLen,0
	;Получение описания устройства
	mov		len,sizeof aBuffer
	invoke CM_Get_DevNode_Registry_Property,dDevInst,CM_DRP_FRIENDLYNAME,0,\
			addr aBuffer, addr len,0
	.if	eax!=CR_SUCCESS
		invoke CM_Get_DevNode_Registry_Property,dDevInst,CM_DRP_DEVICEDESC,0,\
				addr aBuffer, addr len,0
	.endif
	.if	eax!=CR_SUCCESS
		mov	aBuffer[0],0
	.endif
	;выводим информацию
	.if	fFiltering==1
		.if	(Status & DN_REMOVABLE)
			invoke	AddItemDev
		.endif
	.else
		invoke	AddItemDev
	.endif
	;ищем потомков	
	invoke	CM_Get_Child,addr NewDevInst,dDevInst,0
	.if	eax == CR_SUCCESS
		invoke	EnumDevice,NewDevInst
	.endif
	;ищем соседей
	invoke	CM_Get_Sibling,addr NewDevInst,dDevInst,0
	.if	eax == CR_SUCCESS
		invoke	EnumDevice,NewDevInst
	.endif
Ret

EnumDevice EndP

Вызов данной процедуры происходит следующим образом:

;получаем корневой элемент
invoke	CM_Locate_DevNode,addr dnDevInst,NULL,CM_LOCATE_DEVNODE_NORMAL
;запускаем обход дерева
invoke	EnumDevice,dnDevInst

По полученному полю Status можно выяснить, предусмотрено ли безопасное извлечение устройства.

.if	(Status & DN_REMOVABLE)
	;Устройство можно извлечь
.else
	;Извлечение не поддерживается
.endif

Следующий код является ключевым, именно он и позволяет безопасно отключить найденное устройство.

………………
;по идентификатору устройства узнаем его дескриптор 
invoke CM_Request_Device_Eject, dnDevInst, NULL, NULL, NULL, NULL
………………

При таким вызове система сама оповестит пользователя о том, что диск больше не доступен или о том, что в данный момент устройство используется и не может быть извлечено. Если необходимо что бы система не реагировала, то можно вызвать функцию с такими параметрами:

.data
pVetoType dd 0
pVetoName db 5 dup (0)
.code
………………
mov	pVetoType,0
mov	pVetoName[0],0
invoke CM_Request_Device_Eject,dnDevInst,addr pVetoType,addr pVetoName,MAX_PATH,NULL
………………

Также предусмотрена возможность получения иконки, ассоциированной с каким либо устройством (иконки можно видеть в диспетчере устройств). Для этого лишь необходим GUID класса устройства. Его можно получить, например, функцией CM_Get_DevNode_Registry_Property с параметром CM_DRP_CLASSGUID.

.data
ClassImageList	ClassImageListData <>
.code
………………
invoke SetupDiGetClassImageList,addr ClassImageList
………………
invoke SetupDiGetClassImageIndex,addr ClassImageList,addr ClassGUID,addr dwImageIndex
………………
invoke SetupDiDestroyClassImageList,addr ClassImageList

Следующий важный момент - как отловить момент подключения отключения устройства. Для этого существует специальное сообщение WM_DEVICECHANGE посылаемое всем главным окнам в системе.

.if eax == WM_DEVICECHANGE
	mov    eax,wParam
	.if eax == DBT_DEVICEARRIVAL 
		mov  eax,lParam
		assume eax:ptr _DEV_BROADCAST_HDR
		mov  eax,[eax].dbch_devicetype
		assume eax:nothing
		.if  eax == DBT_DEVTYP_VOLUME 
			mov eax,lParam
			assume eax:ptr _DEV_BROADCAST_VOLUME
			mov edx,[eax].dbcv_unitmask
			xor ebx,ebx
			.while ebx<26
				mov ecx,edx
				and edx,01h
				.if edx==1
					mov cx,word ptr [eax].dbcv_flags
					.if cx == 00h
						; Добавлен новый диск
					.endif
					.break
				.else
					mov edx,ecx
					shr edx,1
				.endif
				inc ebx
			.endw
		assume eax:nothing
		.endif
	.elseif eax == DBT_DEVNODES_CHANGED
		; Добавлено/удалено устройство
	.endif

В приложении к статье можно найти программу, выполняющую вышеперечисленные действия.

Кроме того, можно запретить все устройства хранения для USB. Похожим способом можно отключать и другие устройства.

ChangeStateUSBDevice proc fState:DWORD
LOCAL	hDevInfoSet	:DWORD
LOCAL	dwTmp		:DWORD
LOCAL	BufStr[255]	:BYTE
LOCAL	Result	:DWORD
mov		Result,FALSE
mov		pcp.ClassInstallHeader.cbSize,sizeof SP_CLASSINSTALL_HEADER
mov		pcp.ClassInstallHeader.InstallFunction,DIF_PROPERTYCHANGE
mov		eax,fState
mov		pcp.StateChange,eax
mov		pcp.Scope,DICS_FLAG_GLOBAL
invoke 	SetupDiGetClassDevs,addr DISK_GUID,NULL,NULL,DIGCF_PRESENT
mov		hDevInfoSet,eax
mov		DevInfoData.cbSize,sizeof SP_DEVINFO_DATA
xor		ecx,ecx
loop1:
	push		ecx
	invoke	SetupDiEnumDeviceInfo,hDevInfoSet, ecx, addr DevInfoData
	.if	eax==0
		jmp	@
	.endif

	invoke	SetupDiGetDeviceRegistryProperty,hDevInfoSet,\
					addr DevInfoData,SPDRP_COMPATIBLEIDS,\
					addr dwTmp,addr BufStr,sizeof BufStr,NULL

	invoke	lstrcmp,addr BufStr,offset aUSBSTOR
	.if	eax==0
		invoke	SetupDiSetClassInstallParams,hDevInfoSet,\
				addr DevInfoData,addr pcp,sizeof SP_PROPCHANGE_PARAMS
		.if	eax==0
			jmp	@
		.endif
		
		invoke	SetupDiCallClassInstaller,DIF_PROPERTYCHANGE,\
				hDevInfoSet, addr DevInfoData
		.if	eax==0
			jmp	@
		.endif
		mov		Result,TRUE
	.endif
	pop	ecx
	inc	ecx
jmp	loop1
invoke	SetupDiDestroyDeviceInfoList,hDevInfoSet
@:
mov	eax,Result
Ret

ChangeStateUSBDevice EndP

Приложения:

  • DevEject.rar – Исходный текст программы, перечисляющие устройства и позволяющие безопасно извлечь выбранное устройство.
  • USB_Disable.rar – Программа может отключать/включать USB накопители.
  • Dr_USB.rar – вариация на тему «безопасное извлечение USB-устройств.» Программа в форме меню выводит список извлекаемых устройств с указанием буквы логического диска, если это устройство предназначено для хранения информации.

Литература

2002-2013 (c) wasm.ru