軟體授權、設備指紋、資產管理常需唯一硬體識別碼。C# 透過 System.Management (WMI) 及 P/Invoke NetAPI32 可取得 CPU ID、硬碟序號、MAC 位址。本文完整呈現 HardwareInfo 類別,並討論跨平台、權限、效能等實務考量。
using System;
using System.Management;
using System.Runtime.InteropServices;
namespace Hardware
{
public class HardwareInfo
{
// 取得機器名稱
public string GetHostName() => System.Net.Dns.GetHostName();
// 取得 CPU ID (WMI Win32_Processor)
public string GetCpuID()
{
try
{
using (var mc = new ManagementClass("Win32_Processor"))
using (var moc = mc.GetInstances())
{
foreach (ManagementObject mo in moc)
return mo.Properties["ProcessorId"].Value?.ToString() ?? "";
}
}
catch { }
return "";
}
// 取得第一顆實體硬碟序號 (WMI Win32_PhysicalMedia)
public string GetHardDiskID()
{
try
{
using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMedia"))
{
foreach (ManagementObject mo in searcher.Get())
return mo["SerialNumber"]?.ToString().Trim() ?? "";
}
}
catch { }
return "";
}
// 取得 MAC 位址 (P/Invoke NetAPI32 Netbios)
public string GetMacAddress()
{
string addr = "";
try
{
var lenum = new LANA_ENUM();
var ncb = new NCB { ncb_command = (byte)NCBCONST.NCBENUM };
int cb = Marshal.SizeOf(typeof(LANA_ENUM));
ncb.ncb_buffer = Marshal.AllocHGlobal(cb);
ncb.ncb_length = (ushort)cb;
char uRetCode = Win32API.Netbios(ref ncb);
lenum = (LANA_ENUM)Marshal.PtrToStructure(ncb.ncb_buffer, typeof(LANA_ENUM));
Marshal.FreeHGlobal(ncb.ncb_buffer);
if (uRetCode != (short)NCBCONST.NRC_GOODRET) return "";
for (int i = 0; i < lenum.length; i++)
{
ncb = new NCB { ncb_command = (byte)NCBCONST.NCBRESET, ncb_lana_num = lenum.lana[i] };
uRetCode = Win32API.Netbios(ref ncb);
if (uRetCode != (short)NCBCONST.NRC_GOODRET) continue;
ncb = new NCB
{
ncb_command = (byte)NCBCONST.NCBASTAT,
ncb_lana_num = lenum.lana[i],
ncb_callname = new byte[16] { (byte)'*' }
};
cb = Marshal.SizeOf(typeof(ADAPTER_STATUS)) + Marshal.SizeOf(typeof(NAME_BUFFER)) * 30;
ncb.ncb_buffer = Marshal.AllocHGlobal(cb);
ncb.ncb_length = (ushort)cb;
uRetCode = Win32API.Netbios(ref ncb);
var adapter = (ADAPTER_STATUS)Marshal.PtrToStructure(ncb.ncb_buffer, typeof(ADAPTER_STATUS));
Marshal.FreeHGlobal(ncb.ncb_buffer);
if (uRetCode == (short)NCBCONST.NRC_GOODRET)
{
if (i > 0) addr += ":";
addr += string.Format("{0:X2}{1:X2}{2:X2}{3:X2}{4:X2}{5:X2}",
adapter.adapter_address[0], adapter.adapter_address[1], adapter.adapter_address[2],
adapter.adapter_address[3], adapter.adapter_address[4], adapter.adapter_address[5]);
}
}
}
catch { }
return addr.Replace(' ', '0');
}
// --- P/Invoke 結構體與常數定義 (略,見原始碼) ---
public enum NCBCONST { NCBNAMSZ = 16, MAX_LANA = 254, NCBENUM = 0x37, NRC_GOODRET = 0x00, NCBRESET = 0x32, NCBASTAT = 0x33, NUM_NAMEBUF = 30 }
[StructLayout(LayoutKind.Sequential)] public struct ADAPTER_STATUS { public byte[] adapter_address; /* ...其他欄位...*/ }
[StructLayout(LayoutKind.Sequential)] public struct NAME_BUFFER { public byte[] name; public byte name_num; public byte name_flags; }
[StructLayout(LayoutKind.Sequential)] public struct NCB { public byte ncb_command; public byte ncb_retcode; public byte ncb_lsn; public byte ncb_num; public IntPtr ncb_buffer; public ushort ncb_length; public byte[] ncb_callname; public byte[] ncb_name; public byte ncb_rto; public byte ncb_sto; public IntPtr ncb_post; public byte ncb_lana_num; public byte ncb_cmd_cplt; public byte[] ncb_reserve; public IntPtr ncb_event; }
[StructLayout(LayoutKind.Sequential)] public struct LANA_ENUM { public byte length; public byte[] lana; }
[StructLayout(LayoutKind.Auto)] public struct ASTAT { public ADAPTER_STATUS adapt; public NAME_BUFFER[] NameBuff; }
public class Win32API { [DllImport("NETAPI32.DLL")] public static extern char Netbios(ref NCB ncb); }
}
}
| 識別碼 | API | 優點 | 缺點 | 跨平台 |
|---|---|---|---|---|
| CPU ID | WMI Win32_Processor | 唯一、難偽造 | 需 WMI 服務、虛擬機器可能相同 | 僅 Windows |
| 硬碟序號 | WMI Win32_PhysicalMedia | 實體唯一 | RAID/虛擬磁碟可能無序號、需管理員權限 | 僅 Windows |
| MAC 位址 | NetAPI32 Netbios | 網路介面唯一 | 多網卡、虛擬網卡、可偽造、P/Invoke 複雜 | Linux 需解析 /sys/class/net/*/address |
// 取得所有網路介面 MAC (跨平台)
var macs = NetworkInterface.GetAllNetworkInterfaces()
.Where(n => n.OperationalStatus == OperationalStatus.Up && n.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(n => n.GetPhysicalAddress().ToString())
.ToList();
// 取得機器唯一 ID (跨平台,需引用 Microsoft.Extensions.Hosting)
var machineId = Environment.MachineName; // 或讀取 /etc/machine-id (Linux)、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid (Windows)
public string GetDeviceFingerprint()
{
var parts = new List<string>();
parts.Add(GetCpuID());
parts.Add(GetHardDiskID());
parts.Add(GetMacAddress());
// 可加入主機板序號、BIOS UUID 等
return string.Join("|", parts.Where(p => !string.IsNullOrEmpty(p)));
}
優點:單一硬體更換不導致指紋完全改變,容錯率高。
Win32_PhysicalMedia 需管理員權限,標準使用者會回傳空字串。Netbios 僅限 Windows,.NET Core / 5+ 需 DllImport("netapi32.dll", ExactSpelling=true, CharSet=CharSet.Ansi)。C# 取得硬體識別碼主要依賴 WMI 與 P/Invoke,三大核心 ID (CPU、硬碟、MAC) 各有優缺。生產系統建議採用 組合指紋 + 快取 + 跨平台抽象層 架構,並在授權邏輯中容許硬體變更(如重新綁定流程)。現代 .NET 應優先使用 NetworkInterface 等跨平台 API,逐步移除 P/Invoke 依賴。