12 February 2011

Sending the SCSI INQUIRY command with DeviceIOControl

Option Strict On
Option Explicit On

Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
Imports System.Security
Imports System.ComponentModel
Imports System.Text

Module Module1

    <SuppressUnmanagedCodeSecurity()> _
    Private Class NativeMethods
        <DllImport("kernel32", SetLastError:=True)> _
        Public Shared Function CreateFile( _
         ByVal FileName As String, _
         ByVal DesiredAccess As Integer, _
         ByVal ShareMode As Integer, _
         ByVal SecurityAttributes As IntPtr, _
         ByVal CreationDisposition As Integer, _
         ByVal FlagsAndAttributes As Integer, _
         ByVal hTemplateFile As IntPtr) As SafeFileHandle
        End Function

        <DllImport("kernel32.dll", SetLastError:=True)> _
        Friend Shared Function DeviceIoControl( _
            ByVal deviceHandle As SafeFileHandle, _
            ByVal controlCode As Integer, _
            ByRef inBuffer As ScsiPassThroughWithBuffers, _
            ByVal inBufferSize As Integer, _
            ByRef outBuffer As ScsiPassThroughWithBuffers, _
            ByVal outBufferSize As Integer, _
            ByRef bytesReturned As Integer, _
            ByVal overlapped1 As IntPtr) As Boolean
        End Function
    End Class

    <StructLayout(LayoutKind.Sequential, pack:=8)> _
    Private Structure ScsiPassThrough
        Public Length As Short
        Public ScsiStatus As Byte
        Public PathId As Byte          ' port / bus
        Public TargetId As Byte        ' controller
        Public Lun As Byte             ' Lun
        Public CdbLength As Byte
        Public SenseInfoLength As Byte
        Public DataIn As Byte
        Public DataTransferLength As Integer
        Public TimeOutValue As Integer
        Public DataBufferOffset As IntPtr
        Public SenseInfoOffset As Integer
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)> _
        Public Cdb() As Byte
        Public Sub Init()
            Me.Length = CShort(Marshal.SizeOf(GetType(ScsiPassThrough)))
            CdbLength = 6                   ' Size of our command structure
            SenseInfoLength = 32            ' Size of our buffer that may be filled with error/status info
            DataIn = 1                      ' SCSI_IOCTL_DATA_IN 
            DataTransferLength = 128        ' Size of our buffer that gets filled with informaion
            TimeOutValue = 10
            ' TargetId, PathId and Lun are filled on return. Don't mean anything on the way out.
            DataBufferOffset = Marshal.OffsetOf(GetType(ScsiPassThroughWithBuffers), "Data")
            SenseInfoOffset = Marshal.OffsetOf(GetType(ScsiPassThroughWithBuffers), "Sense").ToInt32
            Cdb = New Byte(15) {}
            Cdb(0) = &H12
            Cdb(4) = 128
        End Sub
    End Structure

    <StructLayout(LayoutKind.Sequential, pack:=8)> _
    Private Structure ScsiPassThroughWithBuffers
        Public Spt As ScsiPassThrough
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=32)> _
        Public Sense() As Byte
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=128)> _
        Public Data() As Byte
        Public Sub Init()
            Sense = New Byte(31) {}
            Data = New Byte(127) {}
            Spt = New ScsiPassThrough
            Spt.Init()
        End Sub
    End Structure

    Private Enum PeripheralDeviceType
        Sbc2 = 0 ' direct access block device (disk)
        Ssc2 = 1 ' sequential access block device (tape)
        Ssc = 2  ' Printer
        Spc2 = 3 ' Processor
        Sbc = 4  ' Write once
        Mmc = 5  ' CD/DVD etc
        SbcOptical = 7 ' Optical memory device
        Smc2 = 8 ' Medium changer
        scc2 = &HC ' storage array controller (raid)
        Ses = &HD  ' enclosure services device
        Rbc = &HE  ' simple direct access device
        Ocrw = &HF ' optical card reader
        Bcc = &H10 ' bridge controller 
        Osd = &H11 ' object based storage device
        Adc = &H12 ' automation / drive interface
        WellKnown = &H1E
        Unknown = &H1F
    End Enum

    Sub Main()
        Console.WriteLine("Enter a drive letter")
        Dim letter As Char = Console.ReadKey.KeyChar
        Console.WriteLine()
        Const GenericRead As Integer = &H80000000
        Const GenericWrite As Integer = &H40000000
        Const FileShareRead As Integer = 1
        Const FileShareWrite As Integer = 2
        Const OpenExisting As Integer = 3
        Dim drivePath As String = String.Concat("\\.\" & letter & ":")
        Console.WriteLine("Trying path: " & drivePath)
        Using driveHandle As SafeFileHandle = NativeMethods.CreateFile( _
         drivePath, _
         GenericRead Or GenericWrite, _
         FileShareRead Or FileShareWrite, _
         IntPtr.Zero, _
         OpenExisting, _
         0, _
         IntPtr.Zero)
            If driveHandle.IsInvalid Then
                Console.WriteLine("CreateFile ERROR: " & (New Win32Exception).Message)
                Console.ReadKey()
                Return
            End If
            Dim sptwb As New ScsiPassThroughWithBuffers
            sptwb.Init()

            Dim inBufferSize As Integer = Marshal.SizeOf(GetType(ScsiPassThroughWithBuffers))
            Dim bytesReturned As Integer
            Const IOCTL_SCSI_PASS_THROUGH As Integer = &H4D004
            Dim result As Boolean = NativeMethods.DeviceIoControl(driveHandle, IOCTL_SCSI_PASS_THROUGH, _
                sptwb, inBufferSize, sptwb, inBufferSize, bytesReturned, IntPtr.Zero)
            If result = False Then
                Console.WriteLine("DeviceIOControl ERROR: {0} {1}", Marshal.GetLastWin32Error.ToString("x"), (New Win32Exception).Message)
                Console.ReadKey()
                Return
            End If
            Dim bytes() As Byte = sptwb.Data
            Dim peripheralQualifier As Integer = bytes(0) >> 5
            If peripheralQualifier = 1 Then Console.WriteLine("No peripheral is attached to the device")
            Dim peripheralDevice As PeripheralDeviceType = CType(bytes(0) And &H1F, PeripheralDeviceType)
            Console.WriteLine("Peripheral device type: " & peripheralDevice.ToString)
            Console.WriteLine("Version of the standard:" & sptwb.Data(2))
            Console.WriteLine("Additional Length: " & sptwb.Data(4))
            DumpString("Vendor Id: ", sptwb.Data, 8, 8)
            DumpString("Product Id: ", sptwb.Data, 16, 16)
            DumpString("Product Revision Level: ", sptwb.Data, 32, 4)
            DumpString("Serial no: ", sptwb.Data, 36, 8)
            Console.WriteLine("Press any key")
            Console.ReadKey()
        End Using
    End Sub

    Private Sub DumpString(msg As String, bytes() As Byte, offset As Integer, length As Integer)
        Console.WriteLine(String.Format("{0} :'{1}'", msg, ASCIIEncoding.ASCII.GetString(bytes, offset, length)))
    End Sub

End Module

The serial number is unreliable. The peripheral device type tells you which standard the device is using. I’m trying to work out how to get the serial no of my raided or USB drives. This command is getting back the serial number of my controller card for my raid array when I ask it for \\.\C:

Enter a drive letter
c
Trying path: \\.\c:
Peripheral device type: Sbc2
Version of the standard:4
Additional Length: 43
Vendor Id:  :'AMD     '
Product Id:  :'2+0 Stripe/RAID0'
Product Revision Level:  :'1.10'
Serial no:  :'98031612'
Press any key

Getting Hard disk drive info with DeviceIOControl

It’s a pretty popular question on forums – how do I get the serial number for a hard disk drive. The serial number is then used for some form of homebrew security. The consensus amongst those who’ve been programming a while is that it is a waste of time and effort, unreliable and not particularly good at securing your programs…

Anyway, as a fun challenge I had a go at getting the serial number from my laptops ATA-attached drive, by using DeviceIOControl to send the IDENTIFY DEVICE ATA command, as defined in the ATA spec. This won’t work on SCSI drives, or raid or …,… ,…, so it’s not reliable and isn’t the answer for those of you looking for a unique id (there isn’t a reliable unique id).

Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
Imports System.Security
Imports System.ComponentModel
Imports System.Text
 
Module Module1
 
    <SuppressUnmanagedCodeSecurity()> _
    Private Class NativeMethods
        <DllImport("kernel32", SetLastError:=True)> _
        Public Shared Function CreateFile( _
         ByVal FileName As String, _
         ByVal DesiredAccess As Integer, _
         ByVal ShareMode As Integer, _
         ByVal SecurityAttributes As IntPtr, _
         ByVal CreationDisposition As Integer, _
         ByVal FlagsAndAttributes As Integer, _
         ByVal hTemplateFile As IntPtr) As SafeFileHandle
        End Function
 
        <DllImport("kernel32.dll", SetLastError:=True)> _
        Friend Shared Function DeviceIoControl( _
            ByVal deviceHandle As SafeFileHandle, _
            ByVal controlCode As Integer, _
            ByRef inBuffer As ATA_PASS_THROUGH_EX_WITH_BUFFERS, _
            ByVal inBufferSize As Integer, _
            ByRef outBuffer As ATA_PASS_THROUGH_EX_WITH_BUFFERS, _
            ByVal outBufferSize As Integer, _
            ByRef bytesReturned As Integer, _
            ByVal overlapped1 As IntPtr) As Boolean
        End Function
    End Class
 
    <StructLayout(LayoutKind.Sequential)> _
    Private Structure ATA_PASS_THROUGH_EX
        Public Length As Short
        Public AtaFlags As Short
        Public PathId As Byte
        Public TargetId As Byte
        Public Lun As Byte
        Public ReservedAsUchar As Byte
        Public DataTransferLength As Integer
        Public TimeOutValue As Integer
        Public ReservedAsUlong As Integer
        Public DataBufferOffset As IntPtr
        <MarshalAs(UnmanagedType.ByValArray, sizeconst:=8)> _
        Public PreviousTaskFile() As Byte
        <MarshalAs(UnmanagedType.ByValArray, sizeconst:=8)> _
        Public CurrentTaskFile() As Byte
    End Structure
 
    <StructLayout(LayoutKind.Sequential)> _
    Private Structure ATA_PASS_THROUGH_EX_WITH_BUFFERS
        Public Apt As ATA_PASS_THROUGH_EX
        <MarshalAs(UnmanagedType.ByValArray, sizeconst:=512)> _
        Public Data() As Byte
    End Structure
 
    Sub Main()
        Console.WriteLine("Enter a drive letter")
        Dim letter As Char = Console.ReadKey.KeyChar
        Console.WriteLine()
        Const GenericRead As Integer = &H80000000
        Const GenericWrite As Integer = &H40000000
        Const FileShareRead As Integer = 1
        Const FileShareWrite As Integer = 2
        Const OpenExisting As Integer = 3
        Dim drivePath As String = String.Concat("\\.\" & letter & ":")
        Console.WriteLine("Trying path: " & drivePath)
        Using driveHandle As SafeFileHandle = NativeMethods.CreateFile( _
         drivePath, _
         GenericRead Or GenericWrite, _
         FileShareRead Or FileShareWrite, _
         IntPtr.Zero, _
         OpenExisting, _
         0, _
         IntPtr.Zero)
            If driveHandle.IsInvalid Then
                Console.WriteLine("CreateFile ERROR: " & (New Win32Exception).Message)
                Console.ReadKey()
                Return
            End If
            Dim apex As New ATA_PASS_THROUGH_EX
            apex.Length = Marshal.SizeOf(apex)
            apex.AtaFlags = 2 ' ATA_FLAGS_DATA_IN
            apex.DataTransferLength = 512 ' The command returns a 512 byte package of info.
            apex.TimeOutValue = 10 ' 10 second timeout.
            apex.DataBufferOffset = Marshal.OffsetOf(GetType(ATA_PASS_THROUGH_EX_WITH_BUFFERS), "Data")
            apex.CurrentTaskFile = New Byte(7) {} ' This contains the command we are requesting.
            apex.CurrentTaskFile(6) = &HEC        ' <-- the command "IDENTIFY DEVICE"
            Dim apexb As New ATA_PASS_THROUGH_EX_WITH_BUFFERS
            apexb.Apt = apex
            Dim inBufferSize As Integer = Marshal.SizeOf(GetType(ATA_PASS_THROUGH_EX_WITH_BUFFERS))
            Dim bytesReturned As Integer
            Const IOCTL_ATA_PASS_THROUGH As Integer = &H4D02C
            Dim result As Boolean = NativeMethods.DeviceIoControl(driveHandle, IOCTL_ATA_PASS_THROUGH, _
                apexb, inBufferSize, apexb, inBufferSize, bytesReturned, IntPtr.Zero)
            If result = False Then
                Console.WriteLine("DeviceIOControl ERROR: " & (New Win32Exception).Message)
                Console.ReadKey()
                Return
            End If
            DumpString("S/N: ", apexb.Data, 20, 20)
            DumpString("Firmware: ", apexb.Data, 46, 8)
            DumpString("Model: ", apexb.Data, 54, 40)
            Console.WriteLine("Press any key")
            Console.ReadKey()
        End Using
    End Sub
 
    Private Sub DumpString(msg As String, bytes() As Byte, offset As Integer, length As Integer)
        ' The strings are slightly weird - endianness? If you use ASCII.GetBytes then each character 
        ' pair is reversed.
        Dim sb As New StringBuilder(msg & " '")
        For i As Integer = offset To offset + length - 1 Step 2
            sb.Append(Chr(bytes(i + 1)))
            sb.Append(Chr(bytes(i)))
        Next
        sb.Append("'"c)
        Console.WriteLine(sb.ToString)
    End Sub
 
End Module

Output on my laptop:

Enter a drive letter
c
Trying path: \\.\c:
S/N:  '            5TG077W9'
Firmware:  'DE14    '
Model:  'ST9160411ASG                            '
Press any key