https://syscall.eu/blog/2024/08/28/toshiba_hdd_firmware/ Bof. No theme, no regular posting. Archive (c) 2014-2024. Raphael Rigo CC-BY-SA 4.0 About. Upgrading a Toshiba NAS HDD firmware on Linux 28 Aug 2024 TL;DR I reversed the firmware updater of my Toshiba HDD to be able to update it on Linux. The following commands should work, USE AT YOUR OWN RISK: $ wget https://www.canvio.jp/en/support/download/hdd/ot_ihdd/fw/ISFw.dat $ grep -C2 MODELNAME ISFw.dat # ^ # |___ identify the right filename here $ wget https://www.canvio.jp/en/support/download/hdd/ot_ihdd/fw/sk060202.ftd # hdparm --fwdownload-mode3 sk060202.ftd /dev/sdX Context I bought a Toshiba HDWG480 HDD for my NAS. hdparm -I /dev/XXX gives the following output: ATA device, with non-removable media Model Number: TOSHIBA HDWG480 Serial Number: 3430A00RFR0H Firmware Revision: 0601 Transport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions, SATA Rev 2.5, SATA Rev 2.6, SATA Rev 3.0 Standards: Used: unknown (minor revision code 0x006d) Supported: 10 9 8 7 6 5 Likely used: 10 [...] As usual, I wanted to check if any firmware update is available. Toshiba's dedicated webpage lists version 0602 as available for my model. Unfortunately, as expected, there's no firmware update process provided for Linux users, only an "Internal Storage Firmware Update Utility" is provided for Windows. Update files are not provided either. Goals So, our goals are: * understand where the updater gets its update files from * reverse engineer the flashing process itself to reimplement it on Linux Reversing the Windows Updater Intro Running the installer^1 with Wine works perfectly, resulting in the following files being installed under Program Files (x86): 18312 ISFw.exe: PE32 executable (native) Intel 80386, for MS Windows, 4 sections 2434952 TosISFw.exe: PE32 executable (GUI) Intel 80386, for MS Windows, 5 sections 2172296 TosISFwSvc.exe: PE32 executable (GUI) Intel 80386, for MS Windows, 5 sections 2362248 TosISFwTray.exe: PE32 executable (GUI) Intel 80386, for MS Windows, 5 sections A quick look (filename, imports) hints at the following goals for each program: * ISFW.exe is a driver (DriverEntry export), probably in charge of effectively flashing the update. * TosISFw.exe is the GUI * TosISFwSvc.exe is the userland service (as the service related imports show) * TosISFwTray.exe most probably handles a tray icon Finding the update files The obvious move is too grep for URLs in the various installed binaries. Unfortunately, it leads nowhere apart from URLs related to the digital signatures. However, grepping for HttpOpenRequest, an API often used by Windows programs to download files, gives two results: TosISFw.exe and TosISFwSvc.exe. Let's look at TosISFwSvc.exe which is smaller and let's see if we can find the URL by checking the xrefs. The call is in the function at 0x00401040, and looks like this: v15 = HttpOpenRequestW(v14, L"GET", &v36[(_DWORD)lpBuffer], 0, (LPCWSTR)szReferrer, 0, 0x84000000, 0); the function is obviously a "download" helper, as all the API calls show. Let's rename it dlfile. There are only two Xrefs to dlfile: if ( !RegOpenKeyExW( HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\TosISFwSvc", 0, 0x20019u, &phkResult) && readregstring((LPBYTE)&String, &phkResult, L"FwURL") && lstrlenW(&String) ) { sub_401000(); LOBYTE(v47) = 2; if ( dlfile(&String, (int)v38) ) [...] sub_4052E0(&lpValueName, L"%s%d", L"URL", phkResult); v25 = 0; if ( !RegOpenKeyExW( HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\TosISFwSvc", 0, 0x20019u, &v25) && readregstring((LPBYTE)&String, &v25, lpValueName) && lstrlenW(&String) && dlfile(&String, (int)v36) ) The first one gives us our answer: the URL is stored in the registry. It's actually written by the InstallShield setup. disk The value is http://www.canvio.jp/en/support/download/hdd/ot_ihdd/fw/ ISFw.dat Parsing the update file The file is an ini file, which is trivial to read and parse: [VERS] VERSION="20240513" [Firmware] 0000=qa060378.ftd 0000model="TOSHIBA HDWG21E" 0000rev="0603" 0000rev0000="0601" 0000native=0 0000option=0 0001=qa060378.ftd 0001model="TOSHIBA HDWG21C" 0001rev="0603" 0001rev0000="0601" 0001native=0 0001option=0 [...] 0008=sk060202.ftd 0008model="TOSHIBA HDWG480 " 0008rev="0602" 0008rev0000="0601" 0008native=0 0008option=0 [...] ; 905CBD24 in my case, the drive is number 8. What's interesting is the checksum at the end. It's the CRC32 of the file, minus the last 10 bytes, which can be easily checked with the slice and crc32 tools of my hacking Swiss army knife rsbkb: $ slice -- ISFw.dat 0 -10 | crc32 905cbd24 Now obviously, let's try to download the relevant file: $ wget https://www.canvio.jp/en/support/download/hdd/ot_ihdd/fw/sk060202.ftd Resolving www.canvio.jp (www.canvio.jp)... 23.72.248.205, 23.72.248.202 Connecting to www.canvio.jp (www.canvio.jp)|23.72.248.205|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1171456 (1.1M) [...] Just for fun we can check if cpu_rec_rs can identify any code in the binary: $ ~/tools/cpu_rec_rs/cpu_rec_rs sk060202.ftd Loading corpus from "/home/trou/tools/cpu_rec_rs/cpu_rec_corpus/*.corpus" ------------------------------------------------- File | Range | Detected Architecture ------------------------------------------------- sk060202.ftd | Whole file | ARMhf ------------------------------------------------- So the firmware is probably running on an ARM SoC (it is). Understanding the update process Now, how is the file sent to the drive to actually perform the update? Recall that we have 4 binaries and we saw the ISFW.exe is actually a driver. The DriverEntry function is actually extremely simple: NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { int v2; // eax readregistry(); v2 = flashfirmware(); sub_1001812(v2 % 100 == 0, 1, v2); return NtTerminateProcess((HANDLE)0xFFFFFFFF, 0); } I've already renamed readregistry and flashfirmware as the functions are easy to identify: char readregistry() { [...] RtlInitUnicodeString(&DestinationString, L"\\REGISTRY\\MACHINE\\SYSTEM\\CurrentControlSet\\Services\\TosISFwSvc"); [...] if ( NtOpenKey(&KeyHandle, 0x20019u, &ObjectAttributes) >= 0 ) { RtlInitUnicodeString(&ValueName, L"FW_Serial"); if ( NtQueryValueKey(KeyHandle, &ValueName, KeyValuePartialInformation, KeyValueInformation, 0x800u, &ResultLength) >= 0 ) { memcpy(&fwserial, &KeyValueInformation[3], KeyValueInformation[2]); [...] RtlInitUnicodeString(&ValueName, L"FW_CurRev"); memcpy(&fw_cur, &KeyValueInformation[3], KeyValueInformation[2]); [...] RtlInitUnicodeString(&ValueName, L"FW_NewRev"); memcpy(fw_new, &KeyValueInformation[3], KeyValueInformation[2]); [...] RtlInitUnicodeString(&ValueName, L"FW_Model"); memcpy(fw_model, &KeyValueInformation[3], KeyValueInformation[2]); [...] RtlInitUnicodeString(&ValueName, L"FW_FWFile"); wmemcpy(path, L"\\??\\", 4); memcpy(&path + 4, &KeyValueInformation[3], KeyValueInformation[2]); [...] } Registry values (set by TosISFwSvc.exe) are read and copied into global variables, which I renamed according the registry value name. Here's the start of flashfirmware: int flashfirmware() { [...] Handle = 0; fwdata = 0; fwsize = 0; memset(&drivedata, 0, sizeof(drivedata)); printf(L"%s Firmware: %s -> %s\n", fw_model, &fw_cur, fw_new); printf(L"DO NOT TURN OFF THE PC WHILE ANY FIRMWARE UPDATE IS RUNNING.\n"); printf( L"Your device may become unusable if you do this and Toshiba is not \n" "responsible for any damage, including any necessary replacement of \n" "the unit, caused by your doing so.\n"); HeapHandle = RtlCreateHeap(2u, 0, 0, 0, 0, 0); if ( HeapHandle ) { status = readfile(&path, &fwdata, &fwsize); if ( !(status % 100) ) { Handle = verifydisk(&fwserial, &fw_cur, fw_model, &drivedata); [...] verifydisk is very important, yet relatively simple (with everything already renamed): HANDLE __stdcall verifydisk(PCWSTR serial, PCWSTR cur, WCHAR *model, IDENTIFY_DEVICE_DATA *devdata) { HANDLE hdl; // edi UNICODE_STRING cur_; // [esp+10h] [ebp-104h] BYREF struct _UNICODE_STRING serial_; // [esp+18h] [ebp-FCh] BYREF UNICODE_STRING model_from_drive_u; // [esp+20h] [ebp-F4h] BYREF UNICODE_STRING serial_from_drive_u; // [esp+28h] [ebp-ECh] BYREF UNICODE_STRING model_; // [esp+30h] [ebp-E4h] BYREF UNICODE_STRING fwrev_from_drive_u; // [esp+38h] [ebp-DCh] BYREF DWORD *drivenumber; // [esp+40h] [ebp-D4h] HANDLE hdl_; // [esp+44h] [ebp-D0h] char v14; // [esp+4Bh] [ebp-C9h] BYREF WCHAR model_from_drive[50]; // [esp+4Ch] [ebp-C8h] BYREF WCHAR serial_from_drive[30]; // [esp+B0h] [ebp-64h] BYREF WCHAR fwrev_from_drive[18]; // [esp+ECh] [ebp-28h] BYREF [...] for ( drivenumber = 0; (unsigned int)drivenumber < 0x20; drivenumber = (DWORD *)((char *)drivenumber + 1) ) { [...] hdl = opendrive((char)drivenumber); if ( !hdl ) break; if ( !getdevprop(hdl, &bustype) || bustype == BusTypeUsb ) { NtClose(hdl); } else { if ( get_IDENTIFY_DEVICE_DATA(hdl_, devdata, 0x200u) ) { get_drive_serial(devdata, serial_from_drive, 30, 1); get_drive_fw_rev(devdata, fwrev_from_drive, 18, 1); get_drive_model(devdata, model_from_drive, 50, 1); RtlInitUnicodeString(&serial_from_drive_u, serial_from_drive); RtlInitUnicodeString(&fwrev_from_drive_u, fwrev_from_drive); RtlInitUnicodeString(&model_from_drive_u, model_from_drive); if ( RtlEqualUnicodeString(&serial_, &serial_from_drive_u, 0) ) { if ( RtlEqualUnicodeString(&cur_, &fwrev_from_drive_u, 0) && RtlEqualUnicodeString(&model_, &model_from_drive_u, 0) ) { return hdl_; } } } NtClose(hdl_); } } return 0; } Interacting with the drive Verifying the drive type Let's dive into the opendrive and getdevprop functions: HANDLE __stdcall opendrive(char Args) { [...] HANDLE FileHandle; // [esp+30h] [ebp-88h] BYREF WCHAR SourceString[64]; // [esp+34h] [ebp-84h] BYREF DestinationString.Length = 0; *(_DWORD *)&DestinationString.MaximumLength = 0; HIWORD(DestinationString.Buffer) = 0; memset(SourceString, 0, sizeof(SourceString)); FileHandle = 0; wsprintf(SourceString, 64, (wchar_t *)L"\\??\\PhysicalDrive%u", Args); RtlInitUnicodeString(&DestinationString, SourceString); [...] NtOpenFile(&FileHandle, 0x100003u, &ObjectAttributes, &IoStatusBlock, 3u, 0x20u); return FileHandle; } char __stdcall getdevprop(HANDLE hdl, char *bustype) { char tmp; // al struct _IO_STATUS_BLOCK IoStatusBlock; // [esp+Ch] [ebp-1018h] BYREF char *bustype_; // [esp+14h] [ebp-1010h] HANDLE FileHandle; // [esp+18h] [ebp-100Ch] char retvalue; // [esp+1Fh] [ebp-1005h] STORAGE_DEVICE_DESCRIPTOR InputBuffer; // [esp+20h] [ebp-1004h] BYREF FileHandle = hdl; bustype_ = bustype; IoStatusBlock.Status = 0; IoStatusBlock.Information = 0; retvalue = 0; memset(&InputBuffer, 0, 0x1000u); if ( NtDeviceIoControlFile( hdl, 0, 0, 0, &IoStatusBlock, IOCTL_STORAGE_QUERY_PROPERTY, &InputBuffer, 0x1000u, &InputBuffer, 0x1000u) < 0 ) { tmp = 0; } else { tmp = InputBuffer.BusType; retvalue = 1; } if ( bustype_ ) *bustype_ = tmp; return retvalue; } opendrive returns a handle on a given PhysicalDrive, which is then used by getdevprop's NtDeviceIoControlFile. Using IDA's "standard enums", I remapped 0x2D1400 to its readable definition: IOCTL_STORAGE_QUERY_PROPERTY. As InputBuffer is set to 0 before the call, the returned data is a STORAGE_DEVICE_DESCRIPTOR structure, which is used by verifydisk to verify if the drive is connected through USB (BusTypeUsb) and bails out if that's the case. Verifying the drive model get_IDENTIFY_DEVICE_DATA is then called by verifydisk: char __stdcall get_IDENTIFY_DEVICE_DATA(HANDLE hdl, void *buff, size_t Size) { struct _IO_STATUS_BLOCK IoStatusBlock; // [esp+Ch] [ebp-3Ch] BYREF HANDLE FileHandle; // [esp+14h] [ebp-34h] char v6; // [esp+1Bh] [ebp-2Dh] ATA_PASS_THROUGH_DIRECT InputBuffer; // [esp+1Ch] [ebp-2Ch] BYREF FileHandle = hdl; IoStatusBlock.Status = 0; v6 = 0; IoStatusBlock.Information = 0; memset(buff, 0, Size); memset(&InputBuffer, 0, sizeof(InputBuffer)); InputBuffer.Length = 0x28; InputBuffer.AtaFlags = ATA_FLAGS_DRDY_REQUIRED|ATA_FLAGS_DATA_IN|ATA_FLAGS_NO_MULTIPLE; InputBuffer.DataTransferLength = Size; InputBuffer.TimeOutValue = 10; InputBuffer.DataBuffer = buff; InputBuffer.CurrentTaskFile[reg_Command] = 0xEC; if ( NtDeviceIoControlFile(hdl, 0, 0, 0, &IoStatusBlock, IOCTL_ATA_PASS_THROUGH_DIRECT, &InputBuffer, 0x28u, &InputBuffer, 0x28u) >= 0 && (InputBuffer.CurrentTaskFile[reg_Status] & 9) == 0 ) { return 1; } return v6; } NtDeviceIoControlFile is now used with IOCTL_ATA_PASS_THROUGH_DIRECT, which as the name implies, sends a raw ATA command to the drive. Actually understanding the request is a bit complex as the ATA_PASS_THROUGH_DIRECT structure specifies both data buffers and "registers" through the CurrentTaskFile field. CurrentTaskFile is an array used to index 8 registers, both as input and output. Using the documentation, we can create two enums to use in IDA: enum ATA_INPUT_REGISTERS : __int32 { reg_Features = 0x0, reg_Sector_Count_in = 0x1, reg_Sector_Number_in = 0x2, reg_Cylinder_Low_in = 0x3, reg_Cylinder_High_in = 0x4, reg_Device_Head_in = 0x5, reg_Command = 0x6, reg_Reserved = 0x7, }; enum ATA_OUTPUT_REGISTERS : __int32 { reg_Error = 0x0, reg_Sector_Count_out = 0x1, reg_Sector_Number_out = 0x2, reg_Cylinder_Low_out = 0x3, reg_Cylinder_High_out = 0x4, reg_Device_Head_out = 0x5, reg_Status = 0x6, reg_Reserved_out = 0x7, }; So the command here is 0xEC. The ATA/ATAPI command set specification, found here, describes the IDENTIFY DEVICE - ECh, PIO Data-In command, which returns a lot of data. Thankfully, Microsoft gives us the IDENTIFY_DEVICE_DATA structure which has everything. The following code then verify we have the "right" drive by comparing the serial, model and firmware version from the returned data to the ones stored in the registry. int __stdcall get_drive_serial(IDENTIFY_DEVICE_DATA *drivedata, wchar_t *dest, int destlen, char stripflag) { return (int)getdrive_data_string( drivedata, dest, destlen, offsetof(IDENTIFY_DEVICE_DATA, SerialNumber), 20, stripflag); } [...] get_drive_serial(devdata, serial_from_drive, 30, 1); [...] RtlInitUnicodeString(&serial_from_drive_u, serial_from_drive); [...] if ( RtlEqualUnicodeString(&serial_, &serial_from_drive_u, 0) ) { if ( RtlEqualUnicodeString(&cur_, &fwrev_from_drive_u, 0) && RtlEqualUnicodeString(&model_, &model_from_drive_u, 0) ) Actually sending the firmware file Once the driver has identified and verified the disk is actually flashable, it proceeds with the actual update: [...] MaxBlocksPerDownloadMicrocodeMode03 = drivedata.MaxBlocksPerDownloadMicrocodeMode03; if ( !drivedata.MaxBlocksPerDownloadMicrocodeMode03 || drivedata.MaxBlocksPerDownloadMicrocodeMode03 == 0xFFFF ) { MaxBlocksPerDownloadMicrocodeMode03 = 128; } else if ( drivedata.MaxBlocksPerDownloadMicrocodeMode03 >= 0x80u ) { MaxBlocksPerDownloadMicrocodeMode03 = 128; } if ( MaxBlocksPerDownloadMicrocodeMode03 >= drivedata.MinBlocksPerDownloadMicrocodeMode03 && MaxBlocksPerDownloadMicrocodeMode03 ) { fwblocks = fwsize >> 9; fwblocks2 = fwsize >> 9; v1 = 60; do { printprogress(); wait((LARGE_INTEGER)500LL); --v1; } while ( v1 ); for ( fwsize = 0; (int)fwsize < 30; ++fwsize ) { currentblock = 0; status = 6000; if ( fwblocks ) { fwdata1 = fwdata; MaxBytesPerDL = MaxBlocksPerDownloadMicrocodeMode03 << 9; while ( 1 ) { printprogress(); blocks_to_flash = fwblocks2 - currentblock; if ( MaxBlocksPerDownloadMicrocodeMode03 < fwblocks2 - currentblock ) blocks_to_flash = MaxBlocksPerDownloadMicrocodeMode03; if ( !ATA_CMD_DOWNLOAD_MICRO(Handle, currentblock, blocks_to_flash, fwdata1) ) break; currentblock += MaxBlocksPerDownloadMicrocodeMode03; fwdata1 += MaxBytesPerDL; if ( currentblock >= fwblocks2 ) goto LABEL_25; } status = 6009; LABEL_25: fwblocks = fwblocks2; } if ( !(status % 100) ) break; v5 = 2; do { printprogress(); wait((LARGE_INTEGER)500LL); --v5; } while ( v5 ); } if ( !(status % 100) ) { if ( get_IDENTIFY_DEVICE_DATA(Handle, &drivedata, 0x200u) ) { get_drive_fw_rev(&drivedata, newfwrev, 18, 1); if ( wcsncmp(fw_new, newfwrev, wcslen(fw_new)) ) status = 6011; } else { status = 6010; } } } else { LABEL_35: status = 6006; } [...] if ( status % 100 ) printf(L"Update Failed. \n"); else printf(L"Update Succeeded. \n"); As you can see, the updater verifies an interesting field from the drive information data: MaxBlocksPerDownloadMicrocodeMode03. Let's check what this means. Sending ATA firmware update commands Documentation The following excerpt from the ATA command set describes the meaning of the field: A.11.5.3.4 DM MAXIMUM TRANSFER SIZE field If: a) the value of the DM MAXIMUM TRANSFER SIZE field (see table A.30) is greater than zero; b) the value of the DM MAXIMUM TRANSFER SIZE field is less than FFFFh; c) the DOWNLOAD MICROCODE SUPPORTED bit (see A.11.5.2.20) is set to one or the DOWNLOAD MICROCODE DMA SUPPORTED bit (see A.11.5.2.6) is set to one; and d) the DM OFFSETS DEFERRED SUPPORTED bit (see A.11.5.3.1) is set to one, or the DM OFFSETS IMMEDIATE SUPPORTED bit (see A.11.5.3.3) is set to one, then the DM MAXIMUM TRANSFER SIZE field indicates the maximum number of 512-byte data blocks permitted by a DOWNLOAD MICROCODE command (see 7.7) or a DOWNLOAD MICROCODE DMA command (see 7.8) that specifies a subcommand of: a) Download with offsets and save microcode for immediate and future use (i.e., 03h); or b) Download with offsets and save microcode for future use (i.e., 0Eh). Otherwise, no maximum is indicated (i.e., there is no maximum number of 512-byte data blocks). The IDENTIFY DEVICE data contains a copy of the DM MAXIMUM TRANSFER SIZE field (see IDENTIFY DEVICE data word 235 in table 45). Of course, we want to check this DOWNLOAD MICROCODE command: The DOWNLOAD MICROCODE command allows the host to alter the device's microcode. The data transferred using the DOWNLOAD MICROCODE command and the DOWNLOAD MICROCODE DMA command is vendor specific. [...] Downloading and activating microcode involves the following steps: 1) download: the host transfers updated microcode data to the device in one or more DOWNLOAD MICROCODE commands or DOWNLOAD MICROCODE DMA commands; 2) save: after receiving the complete updated microcode data, if specified by the download microcode mode, then the device shall save the updated microcode data to nonvolatile storage; and 3) activate: the device begins using the saved or deferred microcode data for the first time after an event specified by the download microcode mode and the saved or deferred microcode data becomes the active microcode data. The BLOCK COUNT field specifies the number of 512-byte data blocks that shall be transferred. The BLOCK COUNT field is specified in the COUNT field and the LBA field (see table 37). DOWNLOAD Subcommands actually define the update behavior: disk Actual code char ATA_CMD_DOWNLOAD_MICRO(HANDLE FileHandle, __int16 currentblock, int blocks_to_flash, void *fwdata) { struct _IO_STATUS_BLOCK IoStatusBlock; // [esp+Ch] [ebp-38h] BYREF char v6; // [esp+17h] [ebp-2Dh] ATA_PASS_THROUGH_DIRECT InputBuffer; // [esp+18h] [ebp-2Ch] BYREF IoStatusBlock.Status = 0; IoStatusBlock.Information = 0; memset(&InputBuffer, 0, sizeof(InputBuffer)); InputBuffer.Length = 0x28; InputBuffer.AtaFlags = ATA_FLAGS_DRDY_REQUIRED|ATA_FLAGS_DATA_OUT|ATA_FLAGS_NO_MULTIPLE; *(_WORD *)&InputBuffer.CurrentTaskFile[reg_Sector_Count_in] = blocks_to_flash;// BLOCK COUNT *(_WORD *)&InputBuffer.CurrentTaskFile[reg_Cylinder_Low_in] = currentblock;// BUFFER OFFSET v6 = 0; InputBuffer.DataTransferLength = blocks_to_flash << 9; InputBuffer.TimeOutValue = 70; InputBuffer.DataBuffer = fwdata; InputBuffer.CurrentTaskFile[reg_Features] = 3;// mode 3 InputBuffer.CurrentTaskFile[reg_Device_Head_in] = 0xE0;// OBSOLETE7|N/A|OBSOLETE5 InputBuffer.CurrentTaskFile[reg_Command] = IDE_COMMAND_DOWNLOAD_MICROCODE; if ( NtDeviceIoControlFile(FileHandle, 0, 0, 0, &IoStatusBlock, IOCTL_ATA_PASS_THROUGH_DIRECT, &InputBuffer, 0x28u, &InputBuffer, 0x28u) >= 0 && (InputBuffer.CurrentTaskFile[6] & 9) == 0 )// status { return 1; } return v6; } As you can see, the ATA_CMD_DOWNLOAD_MICRO just follows the specification. The only weird point is the Device register, which is basically obsolete, but is set to 0xE0. Just to be sure, I checked hdparm source code to see the value set in the command, and indeed, they also set it to 0xE0, so it's probably legacy cruft: enum { ATA_USING_LBA = (1 << 6), ATA_STAT_DRQ = (1 << 3), ATA_STAT_ERR = (1 << 0), }; [...] r->lob.dev = 0xa0 | ATA_USING_LBA; Conclusion So basically, the updater does: * download the list of updates * check if a drive matches, set registry values * the driver takes over and: + checks if the drive is not connected through USB + verifies it's the actual drive specified in the registry using the IDENTIFY DEVICE command + loops and sends the firmware update, 128 512-bytes chunks at a time, using the DOWNLOAD MICROCODE command + verifies the drive was updated using the IDENTIFY DEVICE command Actually doing the update YOLO, I tried on my main NAS drive: # hdparm -I /dev/sdb | grep Firmware Firmware Revision: 0601 # hdparm --fwdownload-mode3 sk060202.ftd --yes-i-know-what-i-am-doing --please-destroy-my-drive /dev/sdb /dev/sdb: fwdownload: xfer_mode=3 min=1 max=4224 size=512 ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... ................................................................................................................................................................................................................................................. . Done. # hdparm -I /dev/sdb | grep Firmware Firmware Revision: 0602 \o/ 1. Version 1.20.0410, MD5: 7cc7dc301f7b8a45cc56ee25e5707cc2, Date: 2023-12-27 -