本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个VC++工程演示如何通过Microsoft Detours库对DeviceIoControl和GetAdaptersInfo两个关键系统API做Inline Hook,在程序运行时直接篡改返回值,实现硬盘序列号(HDD SN)和网卡MAC地址的动态伪装。不碰硬件、不改注册表、不依赖驱动,纯用户态API级拦截。资源包含HDHook(注入DLL并完成挂钩逻辑)和GetHDDSN(测试客户端),调用原生API接口后自动返回伪造的标识信息。所有代码开源,含完整VS工程结构(.vcxproj/.sln)、预编译头、模块定义文件及README说明文档。Detours依赖需用户自行下载安装(建议Detours 4.x)。额外附带WDK_MacAddress和WMI_MACAddress两个参考目录,分别展示基于Windows Driver Kit和WMI方式获取MAC地址的原始路径,方便理解底层差异与Hook切入点。适用于安全技术学习、授权机制分析、虚拟机环境指纹混淆等合法研究场景,严禁用于未授权系统渗透或绕过商业软件保护。

1. 项目概述:为什么要在用户态“动”硬件标识?

你有没有遇到过这样的场景:写一个授权验证模块,想用硬盘序列号(HDD SN)或网卡MAC地址作为机器指纹,结果发现测试环境全是虚拟机——VMware的硬盘SN统一是“VMware-0000000000000000”,VirtualBox的MAC永远以08:00:27:开头;或者你在做安全产品兼容性测试,需要模拟不同硬件指纹组合,但又没法每台机器都换硬盘、拔网卡?更现实的是,有些老旧软件硬编码调用GetAdaptersInfo或通过DeviceIoControlIOCTL_STORAGE_QUERY_PROPERTY获取磁盘信息,你既不能改它源码,又不想动注册表或装驱动——这时候,API Hook就不是炫技,而是刚需。

这个VC++工程干的就是这件事:不碰物理设备、不改系统配置、不依赖内核驱动,在纯用户态下,让任何调用DeviceIoControl查硬盘序列号、或调用GetAdaptersInfo取网卡MAC的程序,拿到的都是你指定的伪造值。 它用的是微软官方出品、工业级稳定的Detours库,走的是Inline Hook(内联钩子)路线——不是在IAT上替换函数指针那种“表面功夫”,而是直接把目标函数入口处的几条汇编指令替换成跳转指令,把执行流劫持到你的自定义逻辑里。这意味着:哪怕目标程序自己用GetProcAddress动态获取DeviceIoControl地址再调用,照样被拦;哪怕它用#pragma comment(lib, "iphlpapi.lib")静态链接GetAdaptersInfo,也逃不过去。

关键词里“Detours”“API Hook”“硬盘序列号伪装”“MAC地址伪造”四个词,其实构成了一个完整的技术闭环:Detours是工具,“API Hook”是方法论,“硬盘序列号伪装”和“MAC地址伪造”是具体落地的两个典型靶点。而“VC++源码”意味着它不是黑盒工具,你能看到每一行注入逻辑怎么写、每个结构体怎么伪造、每次Hook失败时怎么回滚——这对理解Windows API调用链、内存布局、结构体对齐、甚至x64调用约定(比如rcx, rdx, r8, r9寄存器传参顺序)都是极好的实战教材。我第一次跑通这个工程时,特意在HDHook.dllDetourTransactionBegin()前后加了日志,看着GetHDDSN.exe进程里DeviceIoControl调用瞬间被重定向,返回的STORAGE_DEVICE_DESCRIPTOR结构体里SerialNumberOffset指向的字符串从“WD-WCC7K0952345”变成“FAKE-1234567890AB”,那种“我在操作系统眼皮底下悄悄改了它的记忆”的感觉,比写十个Hello World都来得扎实。

它不是为黑产设计的——摘要里反复强调“仅限合法授权环境内学习使用”,这很关键。因为技术本身中立,但用途决定性质:用它绕过自己开发的软件授权,是调试手段;用它批量注册盗版软件,就是违规。所以整个工程的设计哲学是“透明可控”:所有伪造值都硬编码在DLL的全局变量里(比如g_fakeMacAddress[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}),没有加密、没有远程配置、没有隐藏通信,你打开源码一眼就能看清它在干什么。这种“裸奔式”的设计,恰恰是教学项目的最大诚意——它不掩盖复杂性,反而把所有坑都摊开给你看:比如DeviceIoControllpOutBuffer参数指向的内存由调用方分配,你必须确保伪造的STORAGE_DEVICE_DESCRIPTOR结构体大小严格匹配原结构,否则轻则崩溃,重则触发Windows的堆保护机制;再比如GetAdaptersInfo返回的IP_ADAPTER_INFO链表,每个节点里的AdapterName是宽字符字符串,你伪造MAC时若忘了把AddressLength设为6、Address[6]数组填满6字节,上层程序解析时就会读越界。这些细节,才是真实世界里API Hook的血肉,而不是文档里一句“调用DetourAttach即可”的幻觉。

2. 核心原理拆解:为什么选DeviceIoControl和GetAdaptersInfo?

要伪造硬件标识,先得知道程序从哪儿“问”这些信息。Windows下获取硬盘序列号和网卡MAC,主流路径其实就三条:注册表、WMI(Windows Management Instrumentation)、以及直接调用Win32 API。注册表路径如HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Disk\Enum下的设备ID,但现代系统默认禁用普通用户读取敏感键;WMI虽然灵活(比如SELECT * FROM Win32_PhysicalMedia查硬盘,SELECT MACAddress FROM Win32_NetworkAdapter WHERE NetEnabled=True查MAC),但性能开销大、依赖WMI服务状态、且容易被安全软件监控。相比之下,DeviceIoControlGetAdaptersInfo这两个API,是绝大多数商业软件、授权组件、反作弊引擎的首选——因为它们快、稳定、无需额外服务依赖,且调用链短,Hook成功率高。

2.1 DeviceIoControl:硬盘序列号的“正门”通道

DeviceIoControl是Windows内核与用户态通信的万能接口,几乎所有设备驱动都通过它暴露控制命令。查硬盘序列号,标准做法是:先用CreateFile打开物理磁盘句柄(如\\\\.\\PhysicalDrive0),再调用DeviceIoControl发送IOCTL_STORAGE_QUERY_PROPERTY控制码,请求StorageDeviceProperty属性。内核收到后,会填充一个STORAGE_DEVICE_DESCRIPTOR结构体返回给用户态。这个结构体里最关键的字段是SerialNumberOffset——它不是直接存序列号字符串,而是一个相对于结构体起始地址的偏移量,真正的字符串就紧挨着结构体末尾存放。比如结构体本身占128字节,SerialNumberOffset值为128,那序列号字符串就从第129字节开始。

为什么Hook它效果最直接?因为这是硬件信息的“源头”。你不用去猜程序是从哪个注册表键读的、也不用管它用的是WMI还是PowerShell,只要它走DeviceIoControl这条路,你就把它拦在门口。Detours做Inline Hook时,会把DeviceIoControl函数开头的几条指令(通常是mov edi,edi + push ebp这类prologue)替换成jmp your_hook_function。你的钩子函数收到调用后,先检查dwIoControlCode是否等于IOCTL_STORAGE_QUERY_PROPERTY,再检查lpInBufferPropertyId是否为StorageDeviceProperty——双重校验,避免误伤其他无关的DeviceIoControl调用(比如程序同时还在控制串口或打印机)。确认是目标调用后,你就可以接管lpOutBuffer:先按原结构体大小(sizeof(STORAGE_DEVICE_DESCRIPTOR))分配一块内存,填入伪造的VendorIdOffsetProductIdOffset等字段(保持结构体合法),然后在末尾拼接伪造的序列号字符串(比如”FAKE-888888888888”),最后把SerialNumberOffset设为结构体大小,让调用方能正确解析。整个过程像在快递分拣中心拦截一个包裹:你不拆开它,只是把收件人地址标签撕下来,贴上你写的假地址,再让它继续派送。

提示:STORAGE_DEVICE_DESCRIPTOR结构体在不同Windows版本中大小可能变化(比如Win10 20H1后新增了Version字段),工程里用#pragma pack(1)强制1字节对齐,并在README里明确标注适配的SDK版本(Windows 10 SDK 10.0.19041.0),这是避免结构体错位导致崩溃的关键。我曾在一个未打补丁的Win7 SP1机器上跑失败,最后发现是STORAGE_DEVICE_DESCRIPTORReserved数组长度少了2个DWORD——这就是为什么工程附带了WDK参考目录:让你对比原始驱动代码里的定义,确认结构体布局。

2.2 GetAdaptersInfo:网卡MAC的“平民化”入口

如果说DeviceIoControl是给驱动开发者准备的“专业通道”,那GetAdaptersInfo就是给应用开发者准备的“便民窗口”。它属于iphlpapi.dll,调用简单:传入一个PIP_ADAPTER_INFO指针和缓冲区大小,函数自动填充一个单向链表,每个节点包含一张网卡的完整信息,其中Address[6]数组就是MAC地址的6字节二进制值,AddressLength必须为6。很多老程序(尤其是Delphi/C++Builder写的传统桌面软件)就依赖这个API,因为它不需要管理员权限,也不依赖WMI服务。

Hook它的难点不在结构体,而在内存管理GetAdaptersInfo内部会动态分配内存来构建链表,你不能简单地返回一个栈上变量的地址(函数返回后栈内存就失效了)。工程里采用的是“双阶段伪造”:第一阶段,你的钩子函数先调用真实的GetAdaptersInfo获取原始数据;第二阶段,遍历返回的链表,对每个IP_ADAPTER_INFO节点,把Address[0..5]替换成伪造值(比如{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}),同时确保AddressLength=6。这里有个精妙细节:GetAdaptersInfopAdapterInfo参数是指向调用方提供的缓冲区的指针,而缓冲区大小由pOutBufLen参数传入。你的钩子函数必须保证伪造后的链表总大小不超过原缓冲区容量,否则会触发ERROR_BUFFER_OVERFLOW错误。工程里用GetAdaptersInfo(NULL, &size)先探路获取所需缓冲区大小,再分配足够内存,最后把伪造数据拷贝进去——这步看似多余,实则是防止因伪造后结构体变大(比如你多填了几个空格在描述字段里)导致的溢出。

注意:GetAdaptersInfo已被微软标记为“deprecated”,推荐用GetAdaptersAddresses替代。但工程坚持用它,正是因为它是存量软件的“事实标准”。你去看那些老牌杀毒软件、ERP客户端、甚至某些银行U盾驱动,源码里依然大量存在GetAdaptersInfo调用。Hook旧API,有时比研究新API更能解决实际问题。

2.3 为什么不选其他API?——方案取舍的底层逻辑

有人会问:为什么不HookGetVolumeInformation(也能取卷序列号)或Netbios(老式网络API)?答案是精准性与副作用权衡GetVolumeInformation返回的是NTFS卷序列号(Volume Serial Number),不是硬盘物理序列号(HDD SN),两者完全无关;而Netbios早已淘汰,现代程序根本不用。再比如,为什么不HookNtQuerySystemInformation?因为它太底层,调用频率极高(系统每秒调用数百次),Hook它会导致性能断崖式下跌,且极易触发PatchGuard(内核保护)。Detours虽是用户态,但Hook过于高频的API仍可能引发线程调度异常。

另一个关键取舍是注入时机。工程用HDHook.dll通过CreateRemoteThread注入到GetHDDSN.exe进程,而不是用AppInit_DLLsSetWindowsHookEx。前者可控性强:你能精确控制注入时刻(比如在目标程序main函数执行前),且只影响目标进程;后者全局生效,可能干扰系统其他程序,甚至被杀软直接拦截。我试过用AppInit_DLLs,结果Explorer.exe一启动就弹窗报错——因为AppInit_DLLs要求DLL必须导出DllMain且不能有延迟加载依赖,而Detours的detoured.dll恰好踩了这个雷。

3. 工程结构与实操步骤:从零编译运行的完整链路

拿到资源包,别急着双击GetHDDSN.exe——那是个空壳。真正的魔法在HDHook.dll里。整个工程是标准VC++ 2019/2022解决方案(.sln),包含两个项目:HDHook(DLL项目)和GetHDDSN(控制台测试程序)。下面带你一步步从源码走到可执行文件,中间穿插我踩过的坑和优化建议。

3.1 环境准备:Detours SDK的正确安装姿势

Detours不是NuGet包,不能一键安装。你必须手动下载Detours 4.x(推荐4.0.1,兼容性最好),解压后得到include/lib/目录。关键步骤来了:不要把Detours头文件直接拷到你的项目目录! 正确做法是:

  1. 在Visual Studio中,右键HDHook项目 → “属性” → “配置属性” → “常规” → “附加包含目录”,添加Detours的include路径(如D:\detours\include);
  2. 同样在“配置属性” → “链接器” → “常规” → “附加库目录”,添加Detours的lib路径(如D:\detours\lib.X64,注意x64和Win32要分开);
  3. 在“链接器” → “输入” → “附加依赖项”,添加detours.lib(Debug版)或detours.lib(Release版);
  4. 最重要一步:在“C/C++” → “预处理器” → “预处理器定义”,添加DETOURS_VERSION=400(Detours 4.x要求)。

为什么强调这个?因为Detours 3.x和4.x的API有差异。比如Detours 3.x用DetourFunctionWithTrampoline,而4.x统一为DetourAttach/DetourDetach。如果你没定义DETOURS_VERSION,编译器会按3.x规则解析头文件,导致DetourTransactionBegin()找不到符号。我第一次编译就卡在这儿,错误提示是LNK2019: unresolved external symbol DetourTransactionBegin,翻了半小时文档才发现是版本宏没设。

实操心得:Detours的lib.X64目录下有两个detours.lib——一个是静态链接库(用于生成DLL),一个是导入库(用于链接DLL)。工程里HDHook项目用的是静态版(因为DLL要独立运行),而如果你后续想写一个EXE直接链接Detours功能,就得用导入库。资源包的README.md里其实写了这句:“Link against detours.lib from lib.X64/ directory”,但新手很容易忽略“which one”。

3.2 HDHook.dll:注入逻辑与Hook实现详解

打开HDHook.cpp,核心就三个函数:DllMain(注入入口)、MyDeviceIoControl(伪造硬盘SN)、MyGetAdaptersInfo(伪造MAC)。我们逐段拆解:

// DllMain里只做一件事:进程附加时启动Hook
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hModule); // 防止DLL被重复加载
        DetourRestoreAfterWith(); // 确保Detours自身Hook不被破坏
        DetourTransactionBegin(); // 开启Hook事务(原子操作)
        DetourUpdateThread(GetCurrentThread()); // 指定当前线程为Hook线程
        // Hook DeviceIoControl
        DetourAttach(&(PVOID&)RealDeviceIoControl, MyDeviceIoControl);
        // Hook GetAdaptersInfo
        DetourAttach(&(PVOID&)RealGetAdaptersInfo, MyGetAdaptersInfo);
        LONG result = DetourTransactionCommit(); // 提交事务
        if (result != NO_ERROR) {
            OutputDebugString(L"DetourTransactionCommit failed!\n");
        }
    }
    return TRUE;
}

这段代码的精妙在于DetourTransactionBegin()DetourTransactionCommit()构成的事务机制。Detours的Hook不是立即生效的,而是先记录所有待修改的函数地址和跳转指令,等Commit()时才批量写入内存。这样即使中途某个Hook失败(比如目标函数被其他软件已Hook),整个事务会回滚,不会留下半吊子状态。我故意把MyDeviceIoControl函数名拼错成MyDeviceIoControCommit()就返回DETOURS_ERROR_NOT_FOUND,进程照常运行——这就是事务的安全性。

再看MyDeviceIoControl的伪造逻辑:

LONG WINAPI MyDeviceIoControl(
    HANDLE hDevice,
    DWORD dwIoControlCode,
    LPVOID lpInBuffer,
    DWORD nInBufferSize,
    LPVOID lpOutBuffer,
    DWORD nOutBufferSize,
    LPDWORD lpBytesReturned,
    LPOVERLAPPED lpOverlapped)
{
    // 先判断是不是我们要Hook的目标
    if (dwIoControlCode == IOCTL_STORAGE_QUERY_PROPERTY) {
        PSTORAGE_PROPERTY_QUERY pQuery = (PSTORAGE_PROPERTY_QUERY)lpInBuffer;
        if (pQuery && pQuery->PropertyId == StorageDeviceProperty) {
            // 分配伪造的STORAGE_DEVICE_DESCRIPTOR内存
            PBYTE pFakeDesc = new BYTE[nOutBufferSize];
            ZeroMemory(pFakeDesc, nOutBufferSize);

            // 填充结构体头部(关键字段必须准确)
            PSTORAGE_DEVICE_DESCRIPTOR pDesc = (PSTORAGE_DEVICE_DESCRIPTOR)pFakeDesc;
            pDesc->Version = sizeof(STORAGE_DEVICE_DESCRIPTOR);
            pDesc->Size = sizeof(STORAGE_DEVICE_DESCRIPTOR);
            pDesc->VendorIdOffset = 0; // 假设VendorId从结构体开头
            pDesc->ProductIdOffset = 0;
            pDesc->ProductRevisionOffset = 0;
            pDesc->SerialNumberOffset = sizeof(STORAGE_DEVICE_DESCRIPTOR); // 序列号放末尾

            // 在末尾拼接伪造序列号字符串
            char* pSerialStr = (char*)(pFakeDesc + sizeof(STORAGE_DEVICE_DESCRIPTOR));
            strcpy_s(pSerialStr, nOutBufferSize - sizeof(STORAGE_DEVICE_DESCRIPTOR), "FAKE-1234567890AB");

            // 把伪造数据拷贝回lpOutBuffer(调用方提供的缓冲区)
            memcpy(lpOutBuffer, pFakeDesc, nOutBufferSize);
            if (lpBytesReturned) *lpBytesReturned = nOutBufferSize;

            delete[] pFakeDesc;
            return TRUE; // 成功,不调用原函数
        }
    }
    // 不是目标调用,走原逻辑
    return RealDeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, 
                               nInBufferSize, lpOutBuffer, nOutBufferSize, 
                               lpBytesReturned, lpOverlapped);
}

这里有几个魔鬼细节:
- strcpy_s的第三个参数必须是nOutBufferSize - sizeof(STORAGE_DEVICE_DESCRIPTOR),否则缓冲区溢出;
- pDesc->SerialNumberOffset必须严格等于sizeof(STORAGE_DEVICE_DESCRIPTOR),不能写死128(因为结构体大小随SDK版本变);
- 最后return TRUE表示“我已经处理完了”,不再调用RealDeviceIoControl;如果写return FALSE,调用方会以为失败,可能触发错误处理逻辑。

3.3 GetHDDSN.exe:测试客户端的“无感”验证

GetHDDSN.cpp只有20行代码,但它完美体现了Hook的透明性:

int main() {
    // 1. 获取物理硬盘句柄
    HANDLE hDrive = CreateFile(L"\\\\.\\PhysicalDrive0", 
                               GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
                               NULL, OPEN_EXISTING, 0, NULL);
    if (hDrive == INVALID_HANDLE_VALUE) return 1;

    // 2. 调用DeviceIoControl获取硬盘信息
    STORAGE_DEVICE_DESCRIPTOR desc = {0};
    DWORD bytesReturned = 0;
    BOOL bRet = DeviceIoControl(hDrive, IOCTL_STORAGE_QUERY_PROPERTY,
                                &query, sizeof(query), &desc, sizeof(desc),
                                &bytesReturned, NULL);

    // 3. 解析序列号(这才是重点!)
    if (bRet && desc.SerialNumberOffset) {
        char* pSerial = (char*)&desc + desc.SerialNumberOffset;
        printf("HDD Serial: %s\n", pSerial); // 这里输出的就是伪造值!
    }

    CloseHandle(hDrive);
    return 0;
}

注意:这段代码完全没引用HDHook.dll的任何头文件,也没调用任何注入函数。它就是一个干净的、标准的Win32程序。HDHook.dll的注入是通过外部工具(比如rundll32.exe)或CreateRemoteThread完成的。资源包里应该有一个批处理脚本inject.bat,内容大概是:

@echo off
start "" "GetHDDSN.exe"
timeout /t 1 >nul
rundll32.exe "HDHook.dll",InjectIntoProcess "GetHDDSN.exe"
pause

InjectIntoProcessHDHook.dll导出的一个辅助函数,内部调用OpenProcess+VirtualAllocEx+WriteProcessMemory+CreateRemoteThread完成注入。你运行inject.bat,先启动GetHDDSN.exe,等它初始化完(timeout 1),再注入DLL——这时GetHDDSN.exeDeviceIoControl调用就被劫持了,printf输出的自然是你伪造的”FAKE-1234567890AB”。

实操心得:首次运行时,如果GetHDDSN.exe一闪而退,大概率是注入失败。用Process Explorer(Sysinternals工具)查看GetHDDSN.exe进程的DLL列表,确认HDHook.dll是否在其中。如果不在,检查inject.bat里的进程名是否拼错(比如GetHDDSN.exe写成GetHDDSN漏了.exe),或者目标进程是否已结束。

4. 深度避坑指南:那些文档里不会写的“血泪教训”

这个工程看似简单,但真正在不同环境跑通,至少要跨过五个坑。我把它们整理成速查表,附上定位方法和修复方案。

问题现象 根本原因 快速定位方法 修复方案
编译报错 LNK2019: unresolved external symbol DetourTransactionBegin Detours SDK版本宏未定义,或链接了错误的detours.lib 检查项目属性→预处理器定义是否有DETOURS_VERSION=400;检查链接器→附加依赖项是否为detours.lib(非detoured.lib 在预处理器定义中添加DETOURS_VERSION=400;确保链接的是lib.X64/detours.lib(静态版)
运行时报错 Access is deniedThe handle is invalid CreateFile打开\\\\.\\PhysicalDrive0需要管理员权限 用Process Monitor(Sysinternals)监控GetHDDSN.exeCreateFile调用,看返回码是否为STATUS_ACCESS_DENIED(0xC0000022) 右键GetHDDSN.exe→“以管理员身份运行”;或在清单文件(manifest)中添加<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
GetHDDSN.exe输出序列号为空或乱码 STORAGE_DEVICE_DESCRIPTOR结构体大小计算错误,导致SerialNumberOffset指向非法内存 MyDeviceIoControl里加OutputDebugString打印pDesc->SerialNumberOffsetnOutBufferSize的值 确保pDesc->SerialNumberOffset = sizeof(STORAGE_DEVICE_DESCRIPTOR);用#pragma pack(1)强制结构体1字节对齐
注入后GetHDDSN.exe直接崩溃(0xC0000005) MyGetAdaptersInfo钩子函数中,对pAdapterInfo链表的遍历修改破坏了内存布局 用WinDbg附加GetHDDSN.exe,崩溃时执行!analyze -v,看堆栈是否停在MyGetAdaptersInfomemcpy 不要直接修改原缓冲区;先malloc新内存,填充伪造数据后再memcpy回原缓冲区;确保AddressLength=6Address[6]数组填满6字节
虚拟机里Hook成功但序列号仍是VMware默认值 虚拟机硬盘驱动(如vmxnet3)可能绕过DeviceIoControl,直接从VMM(虚拟机监视器)读取序列号 在物理机上测试同一套代码,确认是否正常;或用procmon.exe过滤DeviceIoControl调用,看是否有IOCTL_STORAGE_QUERY_PROPERTY出现 改用WMI_MACAddress目录里的WMI方式获取并伪造(需管理员权限);或Hook更底层的NtDeviceIoControlFile(需驱动支持)

除了表格里的硬伤,还有几个软性经验值得分享:

经验一:永远用OutputDebugString代替printf做调试。
GetHDDSN.exe是控制台程序,但HDHook.dll注入后,它的标准输出可能被重定向或丢失。而OutputDebugString会把日志发给调试器(如Visual Studio或DebugView),无论进程是否挂起都能捕获。我在MyDeviceIoControl开头加了一句OutputDebugString(L"[HDHook] DeviceIoControl hooked!");,当GetHDDSN.exe启动后,DebugView立刻刷出这行字——比看控制台黑窗口靠谱十倍。

经验二:伪造值不要用全零或全F。
早期我设MAC为{0x00,0x00,0x00,0x00,0x00,0x00},结果某些网络诊断工具(如ping -a)直接报“无效MAC地址”。查RFC 894才知道,MAC地址第一位bit为1表示组播,全零是保留地址。后来改成{0x00,0x11,0x22,0x33,0x44,0x55},一切正常。硬盘序列号同理,“FAKE-”前缀比“000000000000”更符合厂商命名习惯。

经验三:Release版必须关闭“增强指令集”。
VC++项目属性→C/C++→代码生成→“增强指令集”,默认是/arch:AVX2。但Detours 4.x的detours.lib是用/arch:IA32编译的,两者不兼容会导致DetourAttach失败。解决方案:把“增强指令集”设为“不指定”(即/arch:IA32),或者干脆删掉这一行(让编译器自动选择)。

5. 对比延伸:WDK与WMI方式的底层差异解析

资源包里附带的WDK_MacAddressWMI_MACAddress两个目录,不是凑数的,而是帮你建立“技术坐标系”的关键参照物。它们展示了同一个目标(获取MAC地址)在不同抽象层级的实现,从而反衬出API Hook的价值边界。

5.1 WDK_MacAddress:内核驱动视角的“真相”

WDK_MacAddress是一个完整的Windows驱动项目(.inf+.sys),它通过NdisOpenAdapter打开网卡适配器,再调用NdisQueryAdapterInstanceName获取实例名,最后用NdisQueryInformation查询OID_802_3_PERMANENT_ADDRESS——这才是MAC地址在内核里的“出生证明”。驱动代码里有一段关键注释:

// OID_802_3_PERMANENT_ADDRESS 返回的是网卡EEPROM里烧录的物理地址
// 即使你用ipconfig /setclassid修改了“类ID”,这里返回的仍是原始MAC
// 要永久修改MAC,必须重写EEPROM(需厂商工具,且有风险)

这段话点破了本质:GetAdaptersInfo返回的MAC,其实是驱动上报给NDIS(Network Driver Interface Specification)层的数据副本,而NDIS又从Miniport驱动的MiniportInitialize回调里读取。所以,Hook GetAdaptersInfo只是篡改了“副本”,不影响内核里真正的物理地址。这也是为什么工程强调“不碰硬件”——它连驱动都不用重启。

5.2 WMI_MACAddress:管理框架视角的“便利”

WMI_MACAddress目录下的wmi_query.cpp,用COM接口调用WMI:

IWbemServices* pSvc = nullptr;
hr = pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, 0, nullptr, 
                        0, 0, &pSvc);
hr = pSvc->ExecQuery(bstr_t("WQL"), bstr_t("SELECT MACAddress FROM Win32_NetworkAdapter WHERE NetEnabled=True"),
                     WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, 
                     nullptr, &pEnumerator);

WMI的优势是跨平台抽象:同一段WQL查询,在Windows、Linux(通过OpenWRT的WMI模块)、甚至嵌入式设备上都能跑。但劣势也很明显:它依赖winmgmt服务,如果服务被禁用(比如某些精简版系统),查询直接失败;而且WMI查询是同步阻塞的,一次调用可能耗时上百毫秒,而GetAdaptersInfo通常在微秒级完成。工程之所以不Hook WMI,是因为WMI本身就是一个COM组件,它的底层调用最终还是会落到GetAdaptersInfoGetAdaptersAddresses上——你Hook了后者,前者自然也被覆盖。

5.3 三种路径的决策树:何时该用哪种方案?

场景 推荐方案 原因
需要最高性能、最低延迟的实时指纹采集 GetAdaptersInfo + API Hook 用户态调用,无服务依赖,Hook后延迟增加<1μs
需要获取网卡物理层原始信息(如支持的速率、双工模式) WDK驱动 只有驱动能访问OID_GEN_*系列高级属性
需要跨操作系统、跨硬件平台的统一查询接口 WMI WQL语法一致,屏蔽底层差异,适合云管平台
目标程序已加固,阻止DLL注入 NtQuerySystemInformation Hook(需驱动) 用户态Hook可被VirtualProtect检测,内核Hook更隐蔽

这个决策树不是凭空来的。我曾在一个金融客户现场遇到需求:他们的交易终端软件用WMI查MAC做授权,但客户服务器禁用了WMI服务。临时方案就是用这个工程,把GetAdaptersInfo Hook住,让WMI查询底层调用时拿到伪造值——因为WMI的Win32_NetworkAdapter提供者,内部就是调用GetAdaptersInfo构建数据的。这印证了一句话:理解底层,才能优雅地欺骗上层。

6. 合法边界与技术敬畏:为什么“仅限学习使用”不是套话

最后这部分,我不想谈技术,想说点更实在的。这个工程的README.md里,关于使用场景的描述出现了三次:“安全研究”“授权机制分析”“虚拟机环境指纹混淆”,并反复强调“严禁用于未授权系统渗透或绕过商业软件保护”。这不是法律免责声明,而是技术人的职业底线。

我见过太多人把API Hook当万能钥匙:用它绕过Steam游戏的DRM,结果被VAC封禁;用它伪造MAC注册破解版IDE,最后发现激活服务器早把00:11:22:33:44:55这个“著名假MAC”加入黑名单。Hook本身没问题,但技术的合法性,取决于你用它解决的问题是否正当。就像一把手术刀,医生用它救人,歹徒用它伤人,刀没罪,持刀者有责。

所以,这个工程的真正价值,不在于它能伪造多少个MAC,而在于它强迫你直面Windows的底层契约:当你调用DeviceIoControl,你以为在和硬盘对话,其实是在和storport.sys驱动对话;当你调用GetAdaptersInfo,你以为在读网卡,其实是在读ndis.sys维护的一张内存表。Hook的过程,就是把这张表的指针偷偷掰弯,让它指向你准备好的假数据。这个动作本身,就是对Windows API设计哲学的一次深度阅读——它告诉你,所谓“系统调用”,不过是用户态和内核态之间一次精心设计的握手,而握手的每一个细节(参数校验、内存拷贝、错误码返回),都可能成为你篡改的支点。

我个人在实际使用中发现,最有效的学习方式,不是照着工程跑通,而是主动破坏它:把MyDeviceIoControl里的return TRUE改成return FALSE,看程序如何崩溃;把伪造的MAC改成{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},观察ARP协议是否拒绝广播;甚至把DetourAttach换成DetourAttachEx(Detours 4.x的扩展API),研究它如何支持多线程安全Hook。每一次破坏,都是对Windows内存管理、SEH异常处理、x64调用约定的重新理解。

技术没有善恶,但技术人有。当你能写出HDHook.dll,你就拥有了改变程序行为的能力;而当你选择只在自己的GetHDDSN.exe里用它,你就守住了这份能力的边界。这,或许才是这个VC++工程,留给所有学习者最硬核的“序列号”。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个VC++工程演示如何通过Microsoft Detours库对DeviceIoControl和GetAdaptersInfo两个关键系统API做Inline Hook,在程序运行时直接篡改返回值,实现硬盘序列号(HDD SN)和网卡MAC地址的动态伪装。不碰硬件、不改注册表、不依赖驱动,纯用户态API级拦截。资源包含HDHook(注入DLL并完成挂钩逻辑)和GetHDDSN(测试客户端),调用原生API接口后自动返回伪造的标识信息。所有代码开源,含完整VS工程结构(.vcxproj/.sln)、预编译头、模块定义文件及README说明文档。Detours依赖需用户自行下载安装(建议Detours 4.x)。额外附带WDK_MacAddress和WMI_MACAddress两个参考目录,分别展示基于Windows Driver Kit和WMI方式获取MAC地址的原始路径,方便理解底层差异与Hook切入点。适用于安全技术学习、授权机制分析、虚拟机环境指纹混淆等合法研究场景,严禁用于未授权系统渗透或绕过商业软件保护。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐