Windows Laufwerke direkt lesen und schreiben

Liebes GATE-Tagebuch!

Heute wollte ich eigentlich ein Raspbian Image neu herunterladen. Normalerweise nutze ich dann den Win32DiskImager um es auf eine SD-Karte zu übertragen.

Doch dann dachte ich: “Nö, dass will ich jetzt selbst implementieren.” Und schon verzeichnete die GATE-SYSTEM Komponente eine weitere Quelldatei.

Und in Windows mit Geräten zu hantieren war sicher schon der Grund für so manchen unvorhergesehen Suizid. 😛

Eigentlich wäre alles so leicht. Man öffnet die Datei und das Gerät, ließt Daten in einen Puffer und schreibt den Pufferinhalt auf das Gerät… wäre da nicht die wichtigste Frage:
Wie lautet der Pfad eines Gerätes?

Wenn man keine Ahnung hat sucht lange in kryptischen Codes, die anfangs alle keinen Sinn ergeben.
Daher folgt nun des Rätsels Lösung:

  1. Man brauch die Windows-Klassen-GUID für das DISK Interface von Geräten. Und diese Konstante lautet auf
    GUID const GUID_DEVINTERFACE_DISK = { 0x53f56307L, 0xb6bf, 0x11d0, { 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } };
  2. Dann holt man sich ein HDEVINFO-Handle per class_dev_handle= SetupDiGetClassDevs (GUID_DEVINTERFACE_DISK, NULL, NULL, (DIGCF_PRESENT | DIGCF_DEVICEINTERFACE))
  3. Nun beginnen wir eine Schleife, wo ein Index bei 0 beginnend hochzählt und jedes mal die Funktion SetupDiEnumDeviceInterfaces (class_dev_handle, index, &sp_devinfo_data) aufruft, bis sie mit ERROR_NO_MORE_ITEMS fehlschlägt.
  4. Mit der Funktion SetupDiGetDeviceRegistryProperty (class_dev_handle, &sp_devinfo_data, SPDRP_FRIENDLYNAME, &datatype, buffer, buffersize, &buffersizerequired)) erhalten wir den “Friendly-Name” also den anzeigbaren Namen des Datenträgers.
  5. Viel wichtiger ist aber die Funktion SetupDiEnumDeviceInterfaces (class_dev_handle, 0, (GUID*)&GUID_DEVINTERFACE_DISK, index, &sp_interface_device_data) mit der wir den Zugang zu weiteren Details wie den Zugriffspfad erhalten.
  6. Konkret tut das der Aufruf SetupDiGetDeviceInterfaceDetail (class_dev_handle, &sp_interface_device_data, &sp_device_interface_detail_data, buffersize, &bufferrequired, NULL)
  7. Und den Pfad zum Gerät erhalten wir aus dem struct-Member DevicePath. Diesen öffnen wir mit hdisk= CreateFile (sp_device_interface_detail_data.DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL)
  8. Weiter geht es mit DeviceIoControl (hdisk, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &storage_device_number, sizeof(STORAGE_DEVICE_NUMBER), &bufferrequired, NULL) was uns die interne Datenträger-Nummer liefert.
  9. Es gäbe noch einige weitere DeviceIoControl Aufrufe, die uns interessante Daten des Laufwerks liefern:
    • IOCTL_DISK_GET_DRIVE_GEOMETRY_EX lieft uns die Größe des Laufwerks in Bytes, sowie seine Block- bzw. Sektorgröße.
    • IOCTL_STORAGE_QUERY_PROPERTY holt mit der PropertyId = StorageDeviceProperty und dem QueryType = PropertyStandardQuery Daten wie die Seriennummer, die Vendor-Id und die Product-Id in einen Datenpuffer.
  10. Wir benötigen aber die in Punkt 8 beschriebene “storage_device_number”. Mit dieser Zahl bilden wir den String \\.\PhysicalDrive{ID} (z.B.: \\.\PhysicalDrive0) und schließen die geöffnete hdisk mit CloseHandle

Theoretisch könnten wir nun den \\.\PhysicalDriveX Pfad öffnen und Daten lesen oder schreiben.
Doch da gibt es ein Problem: Laufwerke sind ja in Windows meistens eingebunden und somit gibt es Bereiche die durch ACLs oder wegen laufenden Zugriffen gesperrt sind. Also müssen wir zuerst alle Windows-Laufwerke durchgehen und alle mit unserem Zieldatenträger verknüpften sperren, damit wir ungestört auf das Medium direkt zugreifen können.

Ich mache es mir jetzt etwas leicht und ignoriere die Möglichkeiten ein Laufwerk als Unterpfad in einem anderen NTFS Laufwerk zu mounten und gehe nur die Laufwerksbuchstaben A: bis Z: durch. (Sonst haben wir noch eine A4 Seite voller Volume-ID Sucherei.)

Werden die Laufwerke mit CreateFile (_T("\\.\A:"), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL) geöffnet, kann man mit DeviceIoControl und IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS die ID des physischen Datenträgers erhalten, das das Laufwerk beinhaltet. Und das ist die gleiche ID, die wir oben bereits erhalten haben um den Pfad \\.\PhysicalDriveX zu bilden.

Und eben die Laufwerke, die wir sperren wollen, müssen wir mit CreateFile(_T(“\.\A:”), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL) öffnen und mit dem DeviceIoControl Code FSCTL_LOCK_VOLUME sperren. Die Handles behalten wir offen, da ein Schließen die Sperre wieder aufhebt.

Jetzt können wir endlich den Pfad \\.\PhysicalDriveX öffnen. Zum Lesen brauchen wir CreateFile(_T("\\.\PhysicalDriveX"), GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL)
und zum Schreiben CreateFile(_T("\\.\PhysicalDriveX"), GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, OPEN_EXISTING, (FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH), NULL)

Vor jedem ReadFile bzw. WriteFile muss per SetFilePointerEx die Position des nächsten zu lesenden Blocks gesetzt werden und der muss entweder gleich oder ein Vielfaches der Blockgröße des Laufwerks sein.

Und wenn wir fertig sind, müssen wir natürlich wieder alle gesperrten Laufwerke freigeben, in dem wir einfach die zuvor geöffneten und gesperrten Handles schließen.

Fazit: Also, das eigentliche Lesen und Schreiben eines Laufwerks ist einfach, aber an die Pfade und Laufwerkssperren zu kommen ist doch recht mühsam.

Ein gewitzter Linuxianer wird nun sagen:

Pah! Mit dd geht das in einer Zeile.

Nun gut, aber das ist ein fertiges Programm. Und solche gibt es ja auch für Windows und dann haben wir auch nur eine Zeile.

Will man aber unter Linux selbst derartiges schreiben, darf man ebenso lange im System nach Blockgrößen und Pfaden suchen. Nur eines ist unter Linux entspannter: Zum Lesen braucht man dort kein Laufwerk sperren … aber ob das gut ist wenn parallel daran herumgeschrieben wird, ist eine andere Frage…


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!