如何可靠快速地获取网络适配器的MAC地址,仅凭其设备实例ID。

11

给定网络适配器的设备实例ID,我想知道它的MAC地址。 我系统上集成的英特尔千兆网卡的示例设备实例ID:

PCI\VEN_8086&DEV_10CC&SUBSYS_00008086&REV_00\3&33FD14CA&0&C8

到目前为止,我使用的算法工作方式如下:
  1. 使用 DIGCF_DEVICEINTERFACE 调用 SetupDiGetClassDevs
  2. 使用 SetupDiEnumDeviceInfo 获取返回的设备的 SP_DEVINFO_DATA
  3. 使用 GUID_NDIS_LAN_CLASS 调用 SetupDiEnumDeviceInterfaces 来获取设备接口。
  4. 为此返回的设备接口调用 SetupDiGetDeviceInterfaceDetail。这将以字符串形式给我们设备路径:\\?\pci#ven_8086&dev_10cc&subsys_00008086&rev_00#3&33fd14ca&0&c8#{ad498944-762f-11d0-8dcb-00c04fc3358c}\{28fd5409-15bd-4c06-b62f-004d3a06f852}
  5. 此时我们有了网络卡驱动程序接口的地址。使用 #4 的结果调用 CreateFile 打开它。
  6. 使用 IOCTL_NDIS_QUERY_GLOBAL_STATS 和 OID OID_802_3_PERMANENT_ADDRESS 调用 DeviceIoControl 来获取 MAC 地址。
这通常是有效的,在相当多的机器上都已经成功使用。然而,似乎有极少数机器的网络驱动程序无法正确响应步骤#6中的DeviceIoControl请求;即使更新了网络卡驱动程序到最新版本,问题仍然存在。这些是基于Windows 7的较新计算机。具体地,DeviceIoControl成功完成,但返回零字节,而不是预期的包含MAC地址的六个字节。
MSDN页面IOCTL_NDIS_QUERY_GLOBAL_STATS可能提供了一个线索:
此IOCTL将在以后的操作系统发布中被弃用。您应该使用WMI接口查询miniport驱动程序信息。有关更多信息,请参见NDIS支持WMI。
也许更新的网卡驱动程序不再实现此IOCTL?
那么,我应该如何使它起作用?我的方法是否存在疏忽,我做错了一些事情?还是我需要采取非常不同的方法?一些备选方法似乎包括:
  • 查询 Win32_NetworkAdapter WMI 类:虽然提供所需信息,但由于性能差而被拒绝。请参见Fast replacement for Win32_NetworkAdapter WMI class for getting MAC address of local computer
  • 查询 MSNdis_EthernetPermanentAddress WMI 类:看起来是 IOCTL_NDIS_QUERY_GLOBAL_STATS 的 WMI 替代品,并直接从驱动程序查询 OID - 这一方法适用于有问题的网络驱动程序。不幸的是,返回的类实例仅提供 MAC 地址和 InstanceName,这是一个本地化字符串,如 Intel(R) 82567LM-2 Gigabit Network Connection。查询 MSNdis_EnumerateAdapter 会返回一个列表,将 InstanceNameDeviceName 相关联,比如 \DEVICE\{28FD5409-15BD-4C06-B62F-004D3A06F852}。我不知道如何从 DeviceName 转换为即插即用设备实例 ID(PCI\VEN_8086......)。
  • 调用 GetAdaptersAddressesGetAdaptersInfo(已弃用)。在返回值中我可以找到的唯一非本地化标识符是适配器名称,这是一个字符串,如 {28FD5409-15BD-4C06-B62F-004D3A06F852} - 与 WMI NDIS 类返回的 DeviceName 相同。所以,我仍然不知道如何将其与设备实例 ID 相关联。我也不确定它是否会 100% 生效 - 例如对于未配置 TCP/IP 协议的适配器。
  • NetBIOS 方法:需要在卡上设置特定的协议,因此不会 100% 生效。通常看起来很 hack-ish,而且我不知道如何将其与设备实例 ID 相关联。我拒绝采用此方法。
  • UUID 生成方法:由于某些原因被拒绝,我将不在此详细阐述。
似乎如果我能找到一种方法从设备实例ID获取卡的"GUID",那么我就可以用剩下的两种方法中的一种了。但是我还没有想出来如何做。否则,WMI NDIS方法似乎是最有希望的。
获取网络卡和MAC地址的列表很容易,有几种方法可以做到。以快速的方式进行,并让我将其与设备实例ID相关联似乎很难...
编辑:如果有帮助,这是IOCTL调用的示例代码(忽略泄漏的hFile句柄)。
HANDLE hFile = CreateFile(dosDevice.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
    DWORD err = GetLastError();
    wcout << "GetMACAddress: CreateFile on " << dosDevice << " failed." << endl;
    return MACAddress();
}
BYTE address[6];
DWORD oid = OID_802_3_PERMANENT_ADDRESS, returned = 0;
//this fails too: DWORD oid = OID_802_3_CURRENT_ADDRESS, returned = 0;
if (!DeviceIoControl(hFile, IOCTL_NDIS_QUERY_GLOBAL_STATS, &oid, sizeof(oid), address, 6, &returned, NULL)) {
    DWORD err = GetLastError();
    wcout << "GetMACAddress: DeviceIoControl on " << dosDevice << " failed." << endl;
    return MACAddress();
}
if (returned != 6) {
    wcout << "GetMACAddress: invalid address length of " << returned << "." << endl;
    return MACAddress();
}

代码失败,打印:
GetMACAddress: invalid address length of 0.

因此,DeviceIoControl返回非零值表示成功,但随后返回零字节。

3个回答

4
这是一种实现方法:
  1. 调用GetAdaptersAddresses获取IP_ADAPTER_ADDRESSES结构体的列表。
  2. 遍历每个适配器并从AdapterName字段获取其GUID(我不确定这种行为是否有保证,但是我的系统中所有适配器都在此处具有GUID,并且文档说明AdapterName是永久的)。
  3. 对于每个适配器,从HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}\<the adapter GUID>\Connection\PnPInstanceID读取注册表键(如果存在)(从此处获得了这个想法;在Google上搜索该键似乎被很好地记录,因此不太可能更改)。
  4. 从此键中获取适配器的设备ID(类似于:PCI\VEN_14E4&DEV_16B1&SUBSYS_96B11849&REV_10\4&2B8260C3&0&00E4)。
  5. 对每个适配器执行此操作,直到找到匹配项。当您找到匹配项时,只需返回IP_ADAPTER_ADDRESSES并查看PhysicalAddress字段即可。
  6. 喝一杯啤酒(可选)。
如果没有一百万种方法做某件事,那就不是Windows!

2
我猜你想获取MAC地址以实现某种DRM、库存或分类系统,因为你试图获取“永久”的MAC地址而不是当前的地址。
你似乎忘记了甚至有一个“管理上强加的MAC地址”(换句话说:是“强制”的MAC地址)。 一些驱动程序允许您在设备属性页的高级选项卡下执行此操作(例如:我的Marvell网络适配器让我这样做),而其他一些则不允许您这样做(即:它们不支持该属性)。
然而,所有这些都会以注册表值结束:HKLM\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}\xxxx\NetworkAddress,类型为REG_SZ。 在这里,您可以设置一个与原始MAC地址不同的MAC地址,格式为“01020304abcd”(6个字节,纯十六进制,没有:分隔符或0x前缀)。 设置后,重新启动计算机,开机时新的MAC地址将生效。
我恰好有一块带有两个 Marvell 集成网卡和一个 NETGEAR USB WiFi 网卡的主板。其中 Marvell 网卡支持更改 MAC 地址:如果你在注册表中设置了 NetworkAddress 值,你还会在驱动程序属性页面中看到新值,并且它会立即生效,无需重新启动(如果你是从设备属性页更改的)。 下面是使用不同方法读取 MAC 地址的结果:
  • GetAdaptersInfo: 新 MAC 地址
  • IOCTL_NDIS_QUERY_GLOBAL_STATS: 原始 MAC 地址
  • MSNdis_EthernetPermanentAddress: 原始 MAC 地址
我尝试为 NETGEAR USB WiFi 网卡在注册表中添加 NetworkAddress 值,结果如下:
  • GetAdaptersInfo: 新 MAC 地址
  • IOCTL_NDIS_QUERY_GLOBAL_STATS: 新 MAC 地址
  • MSNdis_EthernetPermanentAddress: 新 MAC 地址
原始 MAC 地址已经消失。 因此,为了不被“恶意”用户愚弄,您始终需要检查HKLM\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}\xxxx\NetworkAddress注册表值。如果设置了该值,我想最好根本不要信任该网络适配器,因为驱动程序实现决定使用不同的方法向您呈现什么。

获取该注册表键的一些背景信息:

Microsoft关于HKLM\SYSTEM\CurrentControlSet\Class键的文档
根据该页面上的Microsoft文档,

每个类别都有一个子键,其名称使用安装类的GUID命名

我们选择{4D36E972-E325-11CE-BFC1-08002BE10318}子键(也称为GUID_DEVCLASS_NET,在<devguid.h>中定义,并在此处进一步记录)。
同样,根据Microsoft的文档,

每个类子键包含其他子键,称为该类中安装在系统中的每个设备实例的软件键(或驱动程序键)。每个这些软件键都使用设备实例ID命名,该ID是一个十进制的四位序数值。 xxxx部分是从0开始的正整数的4个字符文本表示形式。

因此,您可以从0000、0001、0002向上遍历子键,直到您系统中的网络适配器数量为止。
文档到此结束:我没有找到关于不同注册表值或类似内容的其他文档。
然而,在这些子键中,您可以找到 REG_SZ 值,可以帮助您将 GetAdaptersInfo()、MSNdis_EthernetPermanentAddress、Win32_NetworkAdapter 和 Device Instance ID 连接起来(这回答了您的问题)。
注册表值包括:
- DeviceInstanceID:其值是 Device Instance ID。 - NetCfgInstanceId:其值是 GetAdaptersInfo() 返回的 IP_ADAPTER_INFO 结构体的 AdapterName 成员。它也是 Win32_NetworkAdapter WMI 类的 GUID 成员。 - 不要忘记 NetworkAddress:如果存在有效的 MAC 地址,则驱动程序可能会将其报告为 GetAdaptersInfo()、MSNdis_EthernetPermanentAddress 和 IOCTL_NDIS_QUERY_GLOBAL_STATS 中正在使用的 MAC 地址!

那么,正如你已经说的那样,MSNdis_EthernetPermanentAddress WMI类与“世界”其余部分之间唯一的连接是通过它的InstanceName成员。您可以将其与GetAdaptersInfo()返回的IP_ADAPTER_INFO结构的Description成员相关联。尽管它可能是一个本地化的名称,但它似乎对于系统是唯一的(对于我的两个集成Marvell NIC,第二个有一个“#2”附加到其名称)。

最后说明:

总之,用户可以选择禁用WMI...


2
我最终使用了SetupDiGetDeviceRegistryProperty来读取SPDRP_FRIENDLYNAME。如果找不到,则读取SPDRP_DEVICEDESC。最终,这让我得到了一个字符串,例如"VirtualBox Host-Only Ethernet Adapter #2"。然后,我将其与WMI NDIS类(MSNdis_EthernetPermanentAddress WMI类)中的InstanceName属性进行匹配。必须读取两个属性,以防有多个共享相同驱动程序的适配器(即"#2"、"#3"等)——如果只有一个适配器,则SPDRP_FRIENDLYNAME不可用,但如果有多个适配器,则需要SPDRP_FRIENDLYNAME来区分它们。
这种方法让我有点紧张,因为我正在比较看起来像是本地化的字符串,并且没有发现任何保证我所做的事情总是有效的文档。不幸的是,我也没有找到任何更好的被记录下来的方法。
另外还有几种替代方法涉及在未记录的注册表位置中查找。其中一种方法是spencercw的方法,另一种方法是读取SPDRP_DRIVER,它是HKLM\SYSTEM\CurrentControlSet\Control\Class下一个子键的名称。在驱动程序键下面,查找Linkage\Export值,然后似乎可以将其与MSNdis_EnumerateAdapter类的DeviceName属性匹配。但是,我没有找到任何文档说明这些值可以合法地匹配。此外,我发现有关Linkage\Export的唯一文档来自Win2000注册表参考,并明确表示应用程序不应依赖它。
另一种方法是看我的原始问题,步骤4: "SetupDiGetDeviceInterfaceDetail返回此设备接口"。实际上,设备接口路径可以用于重建设备路径。从设备接口路径开始:\\?\pci#ven_8086&dev_10cc&subsys_00008086&rev_00#3&33fd14ca&0&c8#{ad498944-762f-11d0-8dcb-00c04fc3358c}\{28fd5409-15bd-4c06-b62f-004d3a06f852}。然后,删除最终斜杠之前的所有内容,只剩下:{28fd5409-15bd-4c06-b62f-004d3a06f852}。最后,在此字符串前面添加\Device\并将其与WMI NDIS类进行匹配。同样,这似乎是未记录的,并且依赖于设备接口路径的实现细节。
最终,我调查的其他方法都有其自己未记录的复杂性,听起来至少与匹配SPDRP_FRIENDLYNAME/SPDRP_DEVICEDESC字符串一样严重。因此,我选择了更简单的方法,即将这些字符串与WMI NDIS类进行匹配。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接