
前两天群里有人问了个很实际的问题易语言怎么取物理网卡的MAC再提炼成一个“特征字”用来做机器码绑定。底下方案五花八门有让用ipconfig /all解析的有让读注册表的还有直接推荐 WMI 的。看着都能跑但真放到多网卡、虚拟机、USB网卡混插的机器上出来的结果经常对不上。我这篇文章就把这块的完整思路理一遍从读取原理到虚拟网卡过滤再到特征字生成规则全讲透。标题里的“物理网卡MAC特征字”其实包含两层意思一层是物理网卡不是虚拟网卡另一层是特征字不是把完整MAC直接拿来用。很多教程只教你“取MAC”不教你“怎么取对MAC”和“取到之后怎么处理”。这篇文章给正准备做软件授权、设备识别、注册码绑定这类功能的开发者看也适合刚接触易语言但想做点正经项目的朋友。1. 为什么做机器绑定时大家总盯着物理网卡MAC做软件授权第一件事就是找“这台机器独一无二的东西”。可选的东西其实不少硬盘序列号、CPU序列号、主板UUID、BIOS编号、网卡MAC。但真正被用得最多的反而是MAC原因很朴素——网卡物理地址在每个网卡出厂时就有同一个局域网上不会重复而且读取接口明确系统原生支持不需要额外的驱动或特权。硬盘序列号的问题是部分固态硬盘的序列号会变尤其是一些主控做了清洗操作之后CPU序列号在桌面平台上早就不是必选项很多CPU根本不暴露稳定的序列号主板UUID倒是稳定但有些组装机的板子厂商会把UUID全部刷成固定字符串反而成了重灾区。相比之下MAC虽然也能被修改但普通用户没有特殊工具时根本不会去动它拿来当特征源最省心。“特征字”这个提法恰恰是因为很多授权功能不能直接用完整MAC。如果你把“AA-BB-CC-DD-EE-FF”原样当作注册码的一部分用户随便看两眼就知道规则了而且17个字符太长放注册窗口里占地方。更关键的是直接暴露MAC给终端用户对用户隐私也不友好。所以常规做法是取到MAC之后经过一次或几次不可逆的变换生成一个固定长度的短字符串这就是特征字。它和MAC有一一对应关系但看不到原始MAC。很多新手在这里有个误区以为取MAC只是为了那串字符其实真正值钱的是“稳定”和“唯一”这两件事。特征字生成规则再花哨如果源头取回来的网卡是虚拟网卡或者今天取一张明天取另一张后面所有验证逻辑全部白搭。所以第一步永远不是写生成函数而是先把“取哪张网卡”这件事定死。2. 取网卡之前先看清三条路API、WMI、命令回显易语言里取MAC归纳起来无非三条路。我一个个说顺便把每条路的坑摆出来。2.1 通过系统API直取GetAdaptersInfo系统自带了一个IP Helper模块里面有个函数专门负责枚举本机网卡适配器叫GetAdaptersInfo。它返回的不是一条MAC字符串而是一整串网卡信息块包括网卡名称、描述文本、MAC字节、接口类型、IP配置等等。这条路的特点是数据最全能拿到网卡描述这是后面过滤虚拟网卡的关键信息。缺点是易语言里用起来比较啰嗦——需要手工声明DLL命令、定义自定义数据类型、处理内存分配和链表遍历。但一旦写成一个子程序以后所有项目都能复用收益很高。2.2 通过WMI查询易受环境和权限影响WMI的Win32_NetworkAdapterConfiguration类也能拿到MAC而且不用处理复杂结构体用易语言的系统对象支持库就能查。部分人喜欢用这条路因为代码看起来短。但WMI的问题也很明显首先它依赖系统组件某些精简过的系统可能查询超时其次部分杀毒软件会静默拦截WMI调用导致返回空值再者通过WMI拿到的网卡集合同样包含虚拟网卡你还得在网络连接ID、MAC、索引之间做二次过滤查出来反而费劲。我不是说它不能用而是它在“需要稳定输出物理网卡”这个场景下不够可控。2.3 通过ipconfig /all解析看起来最省事实则最脆弱运行(ipconfig /all, 假, 1)然后把回显文本抓出来找“物理地址”行这是很多新手的第一反应。确实最快跑通但脆弱得不行中文系统显示“物理地址”英文系统显示“Physical Address”解析顺序不好写多网卡时候文本里好几段容易取错还有虚拟网卡名和无线网卡名混杂。你要是只做自己电脑上的测试这条路没问题但要发给用户跑很容易翻车。三条路我做了个对比方便你选方式数据完整度虚拟网卡识别代码复杂度环境依赖推荐度GetAdaptersInfo高容易有描述文本中高极低推荐WMI查询中较难中中等可选ipconfig解析低困难低受系统语言影响不推荐所以我的结论很明确老老实实用GetAdaptersInfo。它虽然要写多点代码但数据结构规整网卡描述干净过滤规则写一遍就能用十年。下面直接把完整例程写出来。3. 手把手写一个GetAdaptersInfo取物理网卡MAC的易语言例程要调这个API得先在易语言里做两件事定义API声明定义存放数据的结构体。3.1 API声明与自定义数据类型DLL声明很简单注意两个参数都要传指针地址.DLL命令 获取适配器信息, 整数型, iphlpapi.dll, GetAdaptersInfo, 公开 .参数 适配器信息指针, 整数型, 传址 .参数 缓冲区长度指针, 整数型, 传址然后定义结构体。这是整个例程最容易出错的地方字段顺序、大小、类型都不能错尤其是最后那个“下一项指针”少了它链表就断不了。我列一个可直接照抄的版本.数据类型 适配器信息 .成员 组合索引, 整数型 .成员 适配器名称, 字节型, 260 .成员 适配器描述, 字节型, 132 .成员 地址长度, 整数型 .成员 物理地址, 字节型, 8 .成员 适配器索引, 整数型 .成员 接口类型, 整数型 .成员 DHCP启用, 整数型 .成员 当前IP指针, 整数型 .成员 IP地址列表, 字节型, 40 .成员 网关列表, 字节型, 40 .成员 DHCP服务器, 字节型, 40 .成员 是否WINS, 整数型 .成员 主WINS服务器, 字节型, 40 .成员 次WINS服务器, 字节型, 40 .成员 租约时间, 长整数型 .成员 租约到期, 长整数型 .成员 下一项指针, 整数型有人会问后面那堆IP地址、网关、WINS信息我也用不到能不能精简掉不能。这个结构体是整套数据传回来的格式少了任何一段后面的“下一项指针”计算出来的位置就是错的遍历第二张网卡时就会读到垃圾数据。正确的做法是用上面这份完整结构体。3.2 两次调用先问系统要多大缓冲区GetAdaptersInfo的用法是先拿“空指针”调一次系统会返回需要的缓冲区长度按这个长度申请内存后再调一次数据才真正写进来。第一次调用返回值为111即缓冲区溢出这在易语言里是正常流程别当成错误。.子程序 取物理网卡MAC, 文本型 .局部变量 所需长度, 整数型 .局部变量 返回值, 整数型 .局部变量 数据指针, 整数型 .局部变量 当前指针, 整数型 .局部变量 当前信息, 适配器信息 .局部变量 网卡MAC列表, 文本型, , 0 .局部变量 结果MAC, 文本型 返回值 获取适配器信息 (0, 所需长度) 如果真 (返回值 ≠ 111) 返回 (“”) 如果真结束 数据指针 申请内存 (所需长度) 返回值 获取适配器信息 (数据指针, 所需长度) 如果真 (返回值 ≠ 0) 释放内存 (数据指针) 返回 (“”) 如果真结束3.3 遍历链表并收集MAC拿到数据指针后系统在缓冲区里返回的是一串网卡信息块每个块的末尾都有“下一项指针”指向下一块。遍历就是要跟着这个指针一直往下走直到指针为0。当前指针 数据指针 .判断循环首 (当前指针 ≠ 0) 复制内存 (当前指针, 当前信息, 648) 如果真 (是否物理网卡 (当前信息.适配器描述)) 结果MAC 格式化MAC地址 (当前信息.物理地址, 当前信息.地址长度) 加入成员 (网卡MAC列表, 结果MAC) 如果真结束 当前指针 当前信息.下一项指针 .判断循环尾 () 释放内存 (数据指针) 如果真 (取数组成员数 (网卡MAC列表) 0) 返回 (“”) 如果真结束 返回 (网卡MAC列表 [1])这里复制内存是一条易语言API调用作用是按字节把系统缓冲区里的结构体拷贝到易语言变量里。复制大小填648正好是上面结构体的总字节数。当前信息.适配器描述是字节型数组比较前需要转换成文本我后面会给出完整的过滤函数。格式化MAC的子程序核心是逐字节转十六进制文本每两位前补零再统一转大写.子程序 格式化MAC地址, 文本型 .参数 地址数组, 字节型, 数组 .参数 地址长度, 整数型 .局部变量 循环次数, 整数型 .局部变量 第几个, 整数型 .局部变量 字节值, 整数型 .局部变量 高位, 整数型 .局部变量 低位, 整数型 .局部变量 结果, 文本型 结果 “” 如果真 (地址长度 6) 返回 (“”) 如果真结束 计次循环首 (地址长度, 第几个) 字节值 地址数组 [第几个] 高位 字节值 ÷ 16 低位 字节值 % 16 结果 结果 十六进制字符 (高位) 十六进制字符 (低位) 如果真 (第几个 ≠ 地址长度) 结果 结果 “-” 如果真结束 计次循环尾 返回 (到大写 (结果))其中十六进制字符是自己写的小函数把0到15转成“0”-“9”、“A”-“F”。注意易语言里取十六进制文本得到的字符串不一定补零所以手工拆高低四位最稳妥。4. 虚拟网卡和多网卡过滤稳定性的胜负手代码能跑通只是第一步真正决定这玩意儿能不能用的是过滤逻辑。一台电脑上除了物理网卡通常还有虚拟WiFi热点网卡、虚拟机软件创建的虚拟网卡、蓝牙PAN网卡甚至某些安全软件自建的隔离网卡。不过滤的话你今天取到的是物理网卡用户开了一次虚拟机之后取到的就变成虚拟网卡机器码直接漂移。4.1 怎么识别一张网卡是不是物理网卡最依赖字段就是结构体里的“适配器描述”文本也就是系统设备管理器里显示的那行名字。物理网卡描述通常是“Realtek PCIe GbE Family Controller”“Intel(R) Ethernet Connection”这类特征是包含芯片型号和Controller、Connection、Adapter之类的词。虚拟网卡描述则很有规律常见的关键字我整理了一下包含Virtual的基本都是一些虚拟化软件生成的网卡包含VirtualBox、VMware等虚拟机名字的包含Loopback的回环网卡包含Bluetooth的蓝牙虚拟网卡包含某个虚拟机工具名的虚拟网卡另外还有个容易被忽略的判断点物理网卡MAC的字节数一定是6虚拟网卡有些是8字节甚至更多所以代码里地址长度必须等于6这是硬性要求。再有就是MAC不能全为0全0通常是驱动未加载完毕或者系统枚举异常的网卡。4.2 过滤规则的三个层次我建议分三层过滤而不是一个关键字判断就完事第一层地址长度必须等于6。这能直接筛掉一部分奇葩适配器。第二层MAC不能全为0。这个检查成本极低放在最前面最划算。第三层描述文本关键字黑名单。用一点文本包含匹配把上面说的虚拟化、蓝牙、回环关键字全排除掉。三个条件同时满足才认为这张卡是候选物理网卡。.子程序 是否物理网卡, 逻辑型 .参数 描述文本, 文本型 .参数 地址数组, 字节型, 数组 .参数 地址长度, 整数型 .局部变量 描述大写, 文本型 如果真 (地址长度 ≠ 6) 返回 (假) 如果真结束 描述大写 到大写 (描述文本) 如果真 (寻找文本 (描述大写, “VIRTUAL”, , 假) ≠ -1) 返回 (假) 如果真结束 如果真 (寻找文本 (描述大写, “VMWARE”, , 假) ≠ -1) 返回 (假) 如果真结束 如果真 (寻找文本 (描述大写, “VIRTUALBOX”, , 假) ≠ -1) 返回 (假) 如果真结束 如果真 (寻找文本 (描述大写, “LOOPBACK”, , 假) ≠ -1) 返回 (假) 如果真结束 如果真 (寻找文本 (描述大写, “BLUETOOTH”, , 假) ≠ -1) 返回 (假) 如果真结束 返回 (真)注意关键字匹配一定要先转大写因为系统返回的网卡描述可能英文字母大小写不统一。还有几个具体名字我就不写在示例代码里了你自己根据实际见到的网卡描述往黑名单里加。这里要提醒一句这类过滤规则没有一劳永逸的版本新出的虚拟机工具、新出的虚拟网卡驱动都可能用你没见过的描述所以规则要设计成容易增补的形式。4.3 多张物理网卡时取哪张过滤完之后可能还剩两张卡一块板载千兆网卡一块USB网卡。这时候取第一块返回今天能行明天用户换了USB口或者临时拔掉外置网卡机器码就变了。稳妥的做法是把所有通过过滤的网卡MAC都取出来放进数组排序后再组合成特征字。这样USB网卡拔掉、插上只要板载网卡还在特征字就不变。如果用户只有一块USB网卡拔了以后特征字必然会变这是硬件层面的问题不是程序能解决的需要在授权设计时预留换绑机制。我在例程里已经用数组收集了所有MAC列表接着排序拼接成一个长字符串再生成特征字就比只取第一张卡稳定得多。5. 从MAC到特征字别拿完整MAC直接当注册码MAC取对了过滤做对了接下来才轮到特征字生成。这一步看似简单实则有不少讲究。5.1 为什么不能直接拿MAC当特征字直接拼接“AA-BB-CC-DD-EE-FF”当注册码等于把你的身份标识明文发给对方。对方看到规则后自己改个网卡硬件地址就能伪造出另一台机器的码。更直接的问题是不好看、不短。注册窗口里弹出一串17位大写字母加横杠界面粗糙事小用户复制出错事大。特征字要做的是把MAC“打散重排”让输出结果看不出原始结构。常见的做法有三类对MAC字符串做MD5摘要取前8位或前16位计算CRC32转成8位十六进制文本每字节按固定密钥做异或再转文本其中MD5最无脑易语言自带取数据摘要不用额外引模块推荐直接用。为了增加随机性可以在MAC前拼一个固定盐字符串这样同一个MAC在不同软件里生成的码不一样不会被人一眼看出两个软件用的是同一台机器。5.2 生成特征字的完整流程我的做法分三步第一步把过滤后的多张网卡MAC按字母序排序拼成一个无分隔符的长字符串。比如1A2B3C4D5E6F7E6F5D4C3B2A1F。第二步在这个字符串前面加一个项目独有盐比如“MYAPP-2024”拼成待计算文本。第三步对这个文本取MD5摘出前16位作为特征字。如果你觉得16位太长取前8位也行只是碰撞概率会从极低升到可以忽略的级别一般软件绑定场景8位就足够。.子程序 生成特征字, 文本型 .参数 网卡MAC数组, 文本型, 数组 .局部变量 排序后数组, 文本型, , 0 .局部变量 i, 整数型 .局部变量 原始文本, 文本型 .局部变量 加盐文本, 文本型 .局部变量 摘要值, 字节集 排序后数组 网卡MAC数组 数组排序 (排序后数组, 0) 原始文本 “” 计次循环首 (取数组成员数 (排序后数组), i) 去掉横杠再拼接 原始文本 原始文本 删全部空 (子文本替换 (排序后数组 [i], “-”, “”, , , 假)) 计次循环尾 加盐文本 “MYAPP-2024” 原始文本 摘要值 取数据摘要 (到字节集 (加盐文本)) 返回 (取文本左边 (取字节集文本 (摘要值), 16))取数据摘要在易语言里返回的是一段字节集直接转文本就是32位十六进制MD5。这边只取前16位。注意“取字节集文本”这个命令的输出是字符形式的十六进制不需要再手工转。5.3 特征字验证逻辑验证逻辑其实很简单软件运行时再次调用取MAC和生成特征字的函数把输出和注册时保存的特征字比对一致就放行不一致就提示未注册。这套逻辑的关键点是“取MAC的代码路径必须唯一”。万一你在好几个窗口里各写一遍不同的取MAC逻辑用户那里环境一变两边结果对不上自己把自己坑死。后面的删除横杠、转大写、加盐、取摘要这些步骤必须集中封装成一个模块授权窗口、注册窗口、主程序验证调用同一个子程序。这也是整个方案里最容易忽略的工程问题。6. 实测里绕不开的坑以及给后面要做的同学提个醒按上面这套写下来在大多数机器上都能稳定工作。但真放到用户环境里还是有几个反复出现的坑我这里一次性说全。6.1 字节序不是你以为的那个顺序GetAdaptersInfo返回的物理地址是字节数组不是字符串。比如网卡MAC实际是A4 8E DB 32 51 08你组装文本时按这个顺序读就行。但有些网卡驱动或者某些API返回时会把字节序调成反过来的尤其是部分USB外置网卡。我在格式化函数里是按顺序读的如果在你测试的机器上发现MAC反了可以在格式化之前做一个字节倒序处理。怎么判断和你网卡属性里显示的物理地址对照一下就知道了。这里有个通用建议别把MAC字符串当成纯文本处理尽量始终按字节数组处理这样倒序、过滤、异或都好写。6.2 杀毒软件和权限的干扰杀毒软件对读取硬件信息的程序比较敏感尤其是调用WMI和读写注册表时。用GetAdaptersInfo这类系统API相对安全但如果是被某些强防护软件盯上API调用可能会被hook返回的数据被篡改。遇到这种情况没有绝对通用的绕过方案只能让用户把软件加入白名单。我建议在代码里对返回数据进行基础校验比如MAC是否符合十六进制字符规律、长度是否正常异常就直接报错而不是带病运行。权限问题也不能忽略。管理员权限下取到的网卡信息最全非管理员下GetAdaptersInfo通常也能工作但某些精简系统里IP Helper服务被禁用时调用会失败。代码里要处理失败分支不要一失败就崩溃。6.3 用户改了MAC授权系统要能扛住物理网卡MAC可以被用户在设备管理器里手动修改这是客观存在的。少数用户出于隐私或某些工具软件的要求会把MAC改成固定值。这时候特征字会变原来授权的软件会失效。所以做授权系统一定要留“后路”绑定信息不要只存一份特征字建议同时存特征字和机器编码或者允许用户通过授权码联系换绑。没有这个机制光MAC漂移就能让你技术支持忙到崩溃。6.4 我的建议MAC特征字是手段不是终点从实际项目角度说用一个物理网卡MAC生成的特征字来绑定软件授权对大多数中小软件来说够用了。它的优点是实现简单、读取速度快、不依赖额外组件缺点是硬件变更时用户需要人工干预。如果你的授权需求更高比如要防克隆、防虚拟机破解那就不能只依赖MAC应该把硬盘序列号、主板UUID、CPU编号一起拉出来每一路都生成特征字再做一个组合决策。这个方案留给你后续扩展。最后再说一个我个人的细节习惯在整个取MAC、过滤、生成特征字的流程里所有子程序都加日志输出。发布版本里日志可以关掉但保留开关。部署到用户机器上出问题时打开日志看完整链路比远程猜快得多。就这一个小习惯帮我省了无数售后时间。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。