Windows Laufwerke direkt lesen und schreiben
« | 16 Jan 2019 | »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 unvorhergesehenen 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:
- 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 } };
- Dann holt man sich ein
HDEVINFO
-Handle per
class_dev_handle=
SetupDiGetClassDevs
(GUID_DEVINTERFACE_DISK, NULL, NULL, (DIGCF_PRESENT | DIGCF_DEVICEINTERFACE))
- 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 mitERROR_NO_MORE_ITEMS
fehlschlägt. - 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. - 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. - Konkret tut das der Aufruf
SetupDiGetDeviceInterfaceDetail
(class_dev_handle, &sp_interface_device_data, &sp_device_interface_detail_data, buffersize, &bufferrequired, NULL)
- 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)
- 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. - 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 derPropertyId = StorageDeviceProperty
und demQueryType = PropertyStandardQuery
Daten wie die Seriennummer, die Vendor-Id und die Product-Id in einen Datenpuffer.
- 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öffnetehdisk
mitCloseHandle
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 herum geschrieben wird, ist eine andere Frage…