1. What a MAC address is
A MAC address is six bytes. Convention writes them as hex pairs separated by colons:
A4:5E:60:C3:1B:07 The first three bytes are the OUI — the Organizationally Unique Identifier — assigned by the IEEE to the manufacturer. The last three bytes are assigned by the manufacturer to the specific unit.
The OS uses the MAC address to:
- Filter incoming frames (the NIC drops frames not addressed to it)
- Identify itself on the local network segment
- Report to routers, switches, and access points
- Provide a stable identifier for DHCP lease assignment
Spoofing doesn't modify the hardware. It intercepts the OS at some layer between the NIC driver and the network stack and substitutes a different value.
2. Why userspace can't reliably set it
The registry approach
Windows network adapter drivers read the MAC address from a registry value called NetworkAddress during initialization. Setting that value and restarting the adapter works for some drivers.
It fails for others because:
- Some drivers ignore
NetworkAddressentirely - Some read it only at boot, not on adapter restart
- Some read it but report the physical address to the network stack regardless
- Some validate the value against a format whitelist
- Some cache the physical address elsewhere and prefer the cache
From userspace there's no way to know whether the write succeeded, because the adapter can report one address to getmac while using a different one on the wire.
The driver IOCTL approach
Some drivers expose a proprietary IOCTL that changes the MAC. Intel PROSet and Realtek's utility use this. Most drivers don't support it, or support it only through undocumented interfaces, or require the manufacturer's signing certificate to call.
The NDIS OID approach
NDIS defines OID_802_3_CURRENT_ADDRESS for setting the current MAC. Userspace can send OIDs via DeviceIoControl, but on modern Windows the write permission for this OID is restricted to kernel-mode callers. Userspace can query the OID but not set it.
The remaining option
A kernel driver that sits between userspace and the NIC driver and performs the registry write from kernel mode, where the access checks don't apply.
3. The three driver models
WDM — Windows Driver Model
The base model. You create a DRIVER_OBJECT, register dispatch routines for IRP major functions (IRP_MJ_CREATE, IRP_MJ_DEVICE_CONTROL, etc.), create a device object with IoCreateDevice, and handle raw IRPs.
You manage the device object lifecycle, reference counting, I/O queueing, and cleanup manually.
KMDF — Kernel-Mode Driver Framework
A framework layered on WDM. You work with WDFDEVICE, WDFQUEUE, and event callbacks instead of raw IRPs. The framework handles reference counting, PnP, power management, I/O queueing, and cleanup on unload.
NDIS — Network Driver Interface Specification
For network drivers specifically. An NDIS filter driver registers with the NDIS stack, sits between protocol drivers (TCP/IP) and miniport drivers (the NIC), and can intercept every OID and every packet.
Filter drivers have strict requirements around IRQL, packet lifetime, and reentrancy.
4. KMDF implementation
Device extension
Every driver instance has a device extension — a structure attached to the device object holding per-instance state.
typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; UNICODE_STRING DeviceName; UNICODE_STRING SymbolicLink; UCHAR TargetMAC[6]; BOOLEAN IsActive; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; TargetMAC holds the value set by the last IOCTL. IsActive flags whether the last set succeeded.
IOCTL codes
#define IOCTL_SET_MAC_ADDRESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_GET_MAC_ADDRESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) CTL_CODE packs four fields into a 32-bit value:
- Device type —
FILE_DEVICE_UNKNOWN(0x22). A generic device class. - Function code —
0x800and0x801. Values >= 0x800 avoid Microsoft-reserved codes. - Method —
METHOD_BUFFERED. The I/O manager copies user buffers into kernel memory before invoking the driver.Irp->AssociatedIrp.SystemBufferpoints at the kernel copy. - Access —
FILE_ANY_ACCESS. Any user who can open the device can send the IOCTL.
Driver entry
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { WDF_DRIVER_CONFIG config; WDF_DRIVER_CONFIG_INIT(&config, WiFiMacSpoofEvtDeviceAdd); return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE); } WdfDriverCreate registers WiFiMacSpoofEvtDeviceAdd as the device-add callback. The framework calls it when it's ready for the driver to create its device object.
Device add
NTSTATUS WiFiMacSpoofEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit) { WdfDeviceInitSetDeviceType(DeviceInit, FILE_DEVICE_UNKNOWN); WdfDeviceInitSetExclusive(DeviceInit, FALSE); WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered); WDF_OBJECT_ATTRIBUTES attributes; WDF_OBJECT_ATTRIBUTES_INIT(&attributes); attributes.DeviceObjectExtensionType = sizeof(DEVICE_EXTENSION); WDFDEVICE device; NTSTATUS status = WdfDeviceCreate(&DeviceInit, &attributes, &device); if (!NT_SUCCESS(status)) return status; PDEVICE_EXTENSION deviceExtension = WdfObjectGet_DEVICE_EXTENSION(device); RtlZeroMemory(deviceExtension, sizeof(DEVICE_EXTENSION)); deviceExtension->DeviceObject = WdfDeviceWdmGetDeviceObject(device); WDF_IO_QUEUE_CONFIG queueConfig; WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchParallel); queueConfig.EvtIoDeviceControl = WiFiMacSpoofEvtIoDeviceControl; queueConfig.EvtIoStop = WiFiMacSpoofEvtIoStop; status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, NULL); if (!NT_SUCCESS(status)) return status; static const GUID WiFiMacSpoofGuid = {0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}}; status = WdfDeviceCreateDeviceInterface(device, &WiFiMacSpoofGuid, NULL); if (!NT_SUCCESS(status)) return status; return STATUS_SUCCESS; } WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered) sets the buffering mode for all I/O on this device.
WdfIoQueueDispatchParallel allows multiple IOCTLs to run concurrently. Sequential would also work; parallel is only needed for handlers that can overlap safely.
WdfDeviceCreateDeviceInterface registers a device interface GUID. Userspace finds the device by calling SetupDiGetClassDevs with that GUID, or by opening the symbolic link if one is created.
Device control handler
VOID WiFiMacSpoofEvtIoDeviceControl( _In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t OutputBufferLength, _In_ size_t InputBufferLength, _In_ ULONG IoControlCode ) { NTSTATUS status = STATUS_SUCCESS; WDFDEVICE device = WdfIoQueueGetDevice(Queue); PDEVICE_EXTENSION deviceExtension = WdfObjectGet_DEVICE_EXTENSION(device); PVOID inputBuffer = NULL; PVOID outputBuffer = NULL; size_t bytesReturned = 0; switch (IoControlCode) { case IOCTL_SET_MAC_ADDRESS: if (InputBufferLength < 6) { status = STATUS_BUFFER_TOO_SMALL; break; } status = WdfRequestRetrieveInputBuffer(Request, 6, &inputBuffer, NULL); if (!NT_SUCCESS(status)) break; RtlCopyMemory(deviceExtension->TargetMAC, inputBuffer, 6); status = ChangeWiFiMAC(deviceExtension->TargetMAC); deviceExtension->IsActive = NT_SUCCESS(status); break; case IOCTL_GET_MAC_ADDRESS: if (OutputBufferLength < 6) { status = STATUS_BUFFER_TOO_SMALL; break; } status = WdfRequestRetrieveOutputBuffer(Request, 6, &outputBuffer, NULL); if (!NT_SUCCESS(status)) break; RtlCopyMemory(outputBuffer, deviceExtension->TargetMAC, 6); bytesReturned = 6; break; default: status = STATUS_INVALID_DEVICE_REQUEST; break; } WdfRequestCompleteWithInformation(Request, status, bytesReturned); } WdfRequestRetrieveInputBuffer returns a pointer to the kernel copy of the user's input. WdfRequestRetrieveOutputBuffer returns a pointer to the kernel buffer that will be copied back to the user's output buffer after the handler returns.
5. WDM implementation
Driver entry
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNICODE_STRING deviceName; RtlInitUnicodeString(&deviceName, DEVICE_NAME); PDEVICE_OBJECT deviceObject = NULL; NTSTATUS status = IoCreateDevice( DriverObject, sizeof(DEVICE_EXTENSION), &deviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &deviceObject ); if (!NT_SUCCESS(status)) return status; PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension; RtlZeroMemory(deviceExtension, sizeof(DEVICE_EXTENSION)); deviceExtension->DeviceObject = deviceObject; UNICODE_STRING symbolicLink; RtlInitUnicodeString(&symbolicLink, SYMBOLIC_LINK); status = IoCreateSymbolicLink(&symbolicLink, &deviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(deviceObject); return status; } DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl; DriverObject->DriverUnload = UnloadDriver; deviceObject->Flags |= DO_BUFFERED_IO; deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; } IoCreateDevice allocates the device object and the device extension in one block. The third parameter is the device name (\Device\WiFiMacSpoof).
FILE_DEVICE_SECURE_OPEN applies the device's security descriptor to all opens. Without an INF file that sets a specific descriptor, this falls back to defaults.
IoCreateSymbolicLink creates the \DosDevices\WiFiMacSpoof link that userspace CreateFile(L"\\\\.\\WiFiMacSpoof", ...) resolves to.
DO_BUFFERED_IO on the device object makes the I/O manager use METHOD_BUFFERED semantics for all IOCTLs, mirroring what KMDF does via WdfDeviceInitSetIoType.
Clearing DO_DEVICE_INITIALIZING tells the I/O manager the device is ready to receive I/O.
Device control
NTSTATUS DeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp); ULONG inputLength = ioStack->Parameters.DeviceIoControl.InputBufferLength; ULONG outputLength = ioStack->Parameters.DeviceIoControl.OutputBufferLength; ULONG_PTR information = 0; NTSTATUS status = STATUS_SUCCESS; switch (ioStack->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_SET_MAC_ADDRESS: if (inputLength < 6) { status = STATUS_BUFFER_TOO_SMALL; break; } PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer; if (!inputBuffer) { status = STATUS_INVALID_PARAMETER; break; } RtlCopyMemory(deviceExtension->TargetMAC, inputBuffer, 6); status = ChangeWiFiMAC(deviceExtension->TargetMAC); deviceExtension->IsActive = NT_SUCCESS(status); break; case IOCTL_GET_MAC_ADDRESS: if (outputLength < 6) { status = STATUS_BUFFER_TOO_SMALL; break; } PVOID outputBuffer = Irp->AssociatedIrp.SystemBuffer; if (!outputBuffer) { status = STATUS_INVALID_PARAMETER; break; } RtlCopyMemory(outputBuffer, deviceExtension->TargetMAC, 6); information = 6; break; default: status = STATUS_INVALID_DEVICE_REQUEST; break; } Irp->IoStatus.Status = status; Irp->IoStatus.Information = information; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } IoGetCurrentIrpStackLocation returns the current stack location, which holds the parameters for this request.
Irp->AssociatedIrp.SystemBuffer points at the kernel buffer for buffered I/O — the same buffer the KMDF version receives via WdfRequestRetrieveInputBuffer/WdfRequestRetrieveOutputBuffer.
IoCompleteRequest finalizes the IRP and returns it to the I/O manager.
Exception handling
__try { status = ZwOpenKey(&keyHandle, KEY_READ | KEY_WRITE, &attributes); // ... } __except(EXCEPTION_EXECUTE_HANDLER) { if (keyInfo) ExFreePoolWithTag(keyInfo, 'MAC1'); if (keyHandle) ZwClose(keyHandle); return GetExceptionCode(); } Kernel APIs can raise structured exceptions for conditions like an invalid registry path or an access violation. An unhandled exception in kernel mode triggers a bugcheck (blue screen). __try/__except catches the exception, allows cleanup, and returns an error to the caller.
Variables referenced inside the __except block must be declared outside the __try block, because the compiler scopes them separately.
6. NDIS implementation
Filter callbacks
NDIS_STATUS FilterAttach(NDIS_HANDLE FilterModuleContext, NDIS_HANDLE NdisFilterHandle, PNDIS_FILTER_ATTACH_PARAMETERS AttachParameters); VOID FilterDetach(NDIS_HANDLE FilterModuleContext); NDIS_STATUS FilterOidRequest(NDIS_HANDLE FilterModuleContext, PNDIS_OID_REQUEST OidRequest); VOID FilterOidRequestComplete(NDIS_HANDLE FilterModuleContext, PNDIS_OID_REQUEST OidRequest, NDIS_STATUS Status); FilterAttach is called when the filter binds to a miniport. FilterDetach is called when the binding is torn down. FilterOidRequest is called for every OID passing through the filter. FilterOidRequestComplete is called when an OID request completes.
Filter context
typedef struct _FILTER_CONTEXT { UCHAR SpoofedMAC[6]; BOOLEAN IsSpoofing; } FILTER_CONTEXT, *PFILTER_CONTEXT; One instance per miniport binding. Holds the current spoofed MAC and a flag indicating whether spoofing is active.
What completing the filter requires
- Call
NdisFRegisterFilterDriverinDriverEntry, passingFilterAttachandFilterDetach - In
FilterAttach, queryOID_802_3_CURRENT_ADDRESSfrom the miniport and store it inFilterModuleContext - In
FilterOidRequest, interceptOID_802_3_CURRENT_ADDRESSandOID_802_3_PERMANENT_ADDRESSrequests. Complete them locally with the spoofed value instead of forwarding to the miniport - Handle OID set requests by updating the stored spoofed value
The registry path works without any of this. The filter path, if completed, would allow spoofing at the NDIS layer, which is invisible to software querying the adapter through standard OID paths.
7. The registry manipulation
The ChangeWiFiMAC function is the core of the driver.
Opening the class key
RtlInitUnicodeString(®istryPath, L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}"); InitializeObjectAttributes(&attributes, ®istryPath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status = ZwOpenKey(&keyHandle, KEY_READ | KEY_WRITE, &attributes); The GUID {4d36e972-e325-11ce-bfc1-08002be10318} is the class GUID for network adapters. Under this key, subkeys named 0000, 0001, 0002, etc. correspond to individual installed network devices.
OBJ_KERNEL_HANDLE tells the Object Manager to create the handle in the kernel handle table, which prevents it from being inherited by userspace processes.
Enumerating subkeys
ULONG index = 0; ULONG resultLength = 0; while (TRUE) { status = ZwEnumerateKey(keyHandle, index, KeyBasicInformation, NULL, 0, &resultLength); if (status == STATUS_NO_MORE_ENTRIES) break; if (status == STATUS_BUFFER_TOO_SMALL) { keyInfo = ExAllocatePoolWithTag(NonPagedPool, resultLength, 'MAC1'); if (!keyInfo) { status = STATUS_INSUFFICIENT_RESOURCES; break; } status = ZwEnumerateKey(keyHandle, index, KeyBasicInformation, keyInfo, resultLength, &resultLength); if (NT_SUCCESS(status)) { // process subkey } ExFreePoolWithTag(keyInfo, 'MAC1'); } index++; } ZwEnumerateKey with a NULL buffer returns STATUS_BUFFER_TOO_SMALL and sets resultLength to the required size. The code then allocates a buffer of that size and calls again.
The two-pass pattern is standard for kernel registry enumeration.
Reading DriverDesc
UNICODE_STRING valueName; RtlInitUnicodeString(&valueName, L"DriverDesc"); PKEY_VALUE_PARTIAL_INFORMATION valueInfo = NULL; ULONG valueLength = 0; status = ZwQueryValueKey(subkeyHandle, &valueName, KeyValuePartialInformation, NULL, 0, &valueLength); if (status == STATUS_BUFFER_TOO_SMALL) { valueInfo = ExAllocatePoolWithTag(NonPagedPool, valueLength, 'MAC2'); if (valueInfo) { status = ZwQueryValueKey(subkeyHandle, &valueName, KeyValuePartialInformation, valueInfo, valueLength, &valueLength); // valueInfo->Data points at the string } } Same two-pass pattern. KeyValuePartialInformation returns only the value data, not the name. The Data field is a PWCHAR for REG_SZ values.
Matching the WiFi adapter
PWCHAR driverDesc = (PWCHAR)valueInfo->Data; if (wcsstr(driverDesc, L"Wireless") || wcsstr(driverDesc, L"Wi-Fi") || wcsstr(driverDesc, L"WLAN") || wcsstr(driverDesc, L"802.11")) { // this is the WiFi adapter } String matching against DriverDesc. Alternative fields that could be checked:
-
*PhysicalMediaType— aREG_DWORDthat specifies the media type -
MediaType— anotherREG_DWORD -
Characteristics— aREG_DWORDwith bit flags includingNCF_PHYSICALandNCF_VIRTUAL
Writing the MAC
WCHAR macString[13]; WCHAR hexChars[] = L"0123456789ABCDEF"; for (int i = 0; i < 6; i++) { macString[i * 2] = hexChars[(NewMAC[iSource:https://dev.to/__43698c3f021/spoofing-wifi-mac-addresses-with-a-windows-kernel-driver-1mfd Related posts
The Building Is a System: What Software Engineers Can Learn from Construction Site Failures
