首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 讀取硬碟序列號作為軟體授權綁定唯一標識

     編輯:關於C#

為什麼選擇硬碟序列號?

軟體授權常需綁定硬體特徵碼。常見方案有 MAC 地址、CPU ID、主機板序列號、硬碟序列號。其中 硬碟序列號(Volume Serial Number / Physical Disk Serial) 優勢明顯:

  • 幾乎不會變更(除非格式化分區或更換硬碟);
  • 無需管理員權限即可讀取(WMI 需管理員,註冊表不需);
  • 格式固定、長度適中,適合作為授權碼種子。

方法一:註冊表讀取首顆 SCSI 硬碟識別碼(原文方案)

Windows 將硬碟裝置資訊存放於 HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\Logical Unit Id 0(路徑因硬體而異),Identifier 值包含廠商、型號、序列號。原文直接硬編碼路徑:


private void btnGetDiskId_Click(object sender, EventArgs e) {
    string regPath = @"HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 1\Logical Unit Id 0";
    object val = Registry.GetValue(regPath, "Identifier", "Error");
    txtDiskId.Text = val?.ToString() ?? "Not Found";
}

缺點:路徑不固定(Port/Bus/Target/Id 會因插槽、控制器不同而變),通用性差。

方法二:WMI 查詢 Win32_DiskDrive(推薦,健壯跨機器)


using System.Management;

public static string GetFirstDiskSerialWMI() {
    try {
        using (var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_DiskDrive WHERE Index = 0")) {
            foreach (ManagementObject disk in searcher.Get()) {
                string serial = disk["SerialNumber"]?.ToString().Trim();
                if (!string.IsNullOrEmpty(serial)) return serial;
            }
        }
    }
    catch { /* 忽略 WMI 錯誤,回退其他方法 */ }
    return null;
}

需引用 System.Management.dll,專案屬性勾選「允許不安全代碼」可提升效能。

方法三:P/Invoke GetVolumeInformation 取得邏輯卷序列號(最輕量)


using System.Runtime.InteropServices;

public static class NativeMethods {
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool GetVolumeInformation(
        string lpRootPathName,
        StringBuilder lpVolumeNameBuffer,
        uint nVolumeNameSize,
        out uint lpVolumeSerialNumber,
        out uint lpMaximumComponentLength,
        out uint lpFileSystemFlags,
        StringBuilder lpFileSystemNameBuffer,
        uint nFileSystemNameSize);
}

public static uint GetVolumeSerialNumber(string driveLetter = "C") {
    StringBuilder volName = new StringBuilder(260);
    StringBuilder fsName = new StringBuilder(260);
    NativeMethods.GetVolumeInformation(
        driveLetter + ":\\", volName, (uint)volName.Capacity,
        out uint serial, out _, out _, fsName, (uint)fsName.Capacity);
    return serial; // 32 位無符號整數,如 0x1A2B3C4D
}

優點:無需 WMI、極快、無管理員權限需求。缺點:格式化分區會改變,換系統盤授權失效。

方法四:DeviceIoControl 讀取物理硬碟序列號(最權威)


[StructLayout(LayoutKind.Sequential)]
public struct STORAGE_PROPERTY_QUERY {
    public uint PropertyId;
    public uint QueryType;
    public byte[] AdditionalParameters;
}

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern SafeFileHandle CreateFile(
    string lpFileName, uint dwDesiredAccess, uint dwShareMode,
    IntPtr lpSecurityAttributes, uint dwCreationDisposition,
    uint dwFlagsAndAttributes, IntPtr hTemplateFile);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(
    SafeFileHandle hDevice, uint dwIoControlCode,
    ref STORAGE_PROPERTY_QUERY lpInBuffer, uint nInBufferSize,
    IntPtr lpOutBuffer, uint nOutBufferSize,
    out uint lpBytesReturned, IntPtr lpOverlapped);

public static string GetPhysicalDiskSerial() {
    const uint IOCTL_STORAGE_QUERY_PROPERTY = 0x2D1400;
    const uint StorageDeviceProperty = 0;
    const uint PropertyStandardQuery = 0;

    using (var handle = CreateFile("\\\\.\\PhysicalDrive0",
        0, 0x3, IntPtr.Zero, 3, 0, IntPtr.Zero)) {
        if (handle.IsInvalid) return null;

        var query = new STORAGE_PROPERTY_QUERY {
            PropertyId = StorageDeviceProperty,
            QueryType = PropertyStandardQuery
        };
        uint outSize = 512;
        IntPtr outBuf = Marshal.AllocHGlobal((int)outSize);
        try {
            if (DeviceIoControl(handle, IOCTL_STORAGE_QUERY_PROPERTY,
                ref query, (uint)Marshal.SizeOf(query),
                outBuf, outSize, out _, IntPtr.Zero)) {
                // 解析 STORAGE_DEVICE_DESCRIPTOR 結構取得 SerialNumberOffset
                // 此處簡化,實際需按結構偏移讀取字串
                return Marshal.PtrToStringAnsi(outBuf + 40); // 大致偏移,需根據結構精確計算
            }
        }
        finally { Marshal.FreeHGlobal(outBuf); }
    }
    return null;
}

可獲取廠商原始序列號(如 WD-WCC4M0XXXXXX),不隨分區格式化改變,最適合高安全授權。

綜合建議:多重特徵碼組合 + 容錯回退


public static string GenerateMachineFingerprint() {
    var parts = new List();
    parts.Add(GetVolumeSerialNumber("C").ToString("X8"));           // 邏輯卷號
    parts.Add(GetFirstDiskSerialWMI() ?? "NO_WMI");                 // 物理硬碟 WMI
    parts.Add(GetMacAddress() ?? "NO_MAC");                         // 網卡 MAC
    parts.Add(Environment.MachineName);                             // 電腦名
    string raw = string.Join("|", parts);
    using (var sha = System.Security.Cryptography.SHA256.Create()) {
        return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(raw)));
    }
}
  • 組合多來源,單一硬體更換不影響授權驗證(可設定容忍度);
  • SHA256 雜湊統一長度,不洩露原始硬體資訊;
  • 授權伺服器端儲存指紋雜湊,驗證時重新計算比對。

注意事項

  1. 虛擬機/容器:硬碟序列號可能相同或為空,需結合 MAC、UUID、BIOS 序列號綜合判斷。
  2. 權限:WMI/DeviceIoControl 需管理員,VolumeInformation 不需;建議啟動時嘗試高權限讀取,失敗回退低權限方案。
  3. 效能:WMI 查詢約 100-300ms,啟動時異步執行避免卡頓 UI。
  4. 隱私合規:收集硬體資訊屬個人資料範疇,隱私政策需說明,授權碼傳輸必須加密(HTTPS)。

總結

原文註冊表法雖簡短但通用性不足。生產環境建議採用 WMI 物理硬碟序列號為主,Volume Serial 為輔,組合 MAC/BIOS 產生指紋雜湊,並實現分級回退策略。授權邏輯應放在伺服器端驗證,客戶端僅上報指紋,防止本地破解。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved