The first version of this article, “Firewall for Windows,” appeared here in 2002. I revisited it in 2016 because the techniques behind early Windows Packet Filter NDIS-hooking drivers are still useful to understand, even though the operating systems below are now obsolete.
Windows XP x64 introduced Kernel Patch Protection, which made this style of hooking less practical. The old hooking technique is not a recommendation for current systems.
This article describes the period before Windows Vista. It does not cover NDIS 6.x lightweight filter drivers or Windows Filtering Platform (WFP) callout drivers. The code fragments below illustrate historical implementation details; they are not complete drivers or current build instructions.
Network traffic filtering technologies for Windows
Windows 9x/ME and Windows NT/2000/XP expose similar socket and NDIS interfaces, but their network subsystem internals differ substantially. A full comparison is beyond this article. For NT/2000, see chapter 13 of Inside Microsoft Windows 2000, Third Edition, by David A. Solomon and Mark E. Russinovich. The NT network subsystem is more complex, but both families can be divided into a few common parts:
- NDIS. Microsoft and 3Com developed the Network Driver Interface Specification in the late 1980s. NDIS defines interfaces between network protocol drivers and network adapter drivers; a conforming adapter driver is commonly called an NDIS miniport. That common interface reduced work for hardware vendors and could ease support across Windows releases, but portability still depends on the target OS, NDIS version, architecture and features used. The period DDK Network Drivers section describes the documented interfaces and library functions.
- Network protocol drivers. A protocol such as TCP/IP uses NDIS on its lower edge to reach adapters. On the NT/2000/XP systems discussed here, a protocol can expose a Transport Driver Interface (TDI) to clients such as afd.sys, part of the Winsock implementation. TDI requests have rough counterparts in socket operations. For example, a client can establish a remote connection through the following steps:
- The client opens a TDI address endpoint and receives an address object representing a local network address. This roughly corresponds to a Winsock bind operation.
- The client opens a TDI connection endpoint and receives a connection object. This roughly corresponds to creating a Winsock socket.
- The client associates the connection object with an address object by issuing an associate-address TDI request.
- To accept connections, a TDI client issues listen and accept requests, roughly corresponding to the Winsock calls. To connect to a remote system, it issues a connect request, which completes when the connection succeeds or fails.
- User-mode DLLs form the Winsock interface, including ws2_32.dll, msafd.dll and wshtcpip.dll on the systems discussed here.
The filtering options below operate at different layers. A Winsock LSP can inspect calls made through Winsock, while a user-mode application can also configure filtering performed by a kernel component. These approaches do not have the same visibility. In particular, software that bypasses Winsock can bypass an LSP, and an LSP does not see forwarded packets that never pass through the local socket interface.
User-mode traffic filtering
- Winsock Layered Service Provider (LSP). The period MSDN documentation includes an SPI.CPP example. An LSP can identify the process using Winsock and can support tasks such as stream processing or quality-of-service policies for those calls. It cannot enforce a system-wide firewall policy by itself: a program that reaches the transport through another interface, such as TDI on these systems, bypasses the LSP. Forwarded packets that do not traverse the local Winsock path also remain outside its view.
- Windows 2000 Packet Filtering Interface. A user-mode application can install filter descriptors that the TCP/IP stack applies to packets. The interface supports PASS/DROP rules using limited IP-address and port criteria. It is a Windows 2000-era option, not a general substitute for every filtering layer.
- Winsock DLL replacement. Replacing a Winsock DLL is another interception method, but it is fragile and I would not recommend it as a filtering design.
- Global hooking of selected APIs, such as Winsock calls and DeviceIoControl. This is technically possible, but coordinating hooks across processes and interfaces is difficult and can affect stability and security.
Kernel-mode traffic filtering
- Kernel-mode socket filter. On the NT/2000 systems considered here, this intercepts calls from msafd.dll to afd.sys, a kernel component of the Winsock path. Its visibility is still tied to that path, and changes to the AFD interface across Windows versions limit portability.
- TDI filter driver. Implementations differed between Windows 9x/ME and NT/2000. Vireo/Numega VToolsD included a 9x example, so I focus on NT/2000. For TCP/IP filtering on that family, a filter could attach to devices created by tcpip.sys, such as \Device\RawIp, \Device\Udp and \Device\Tcp, or patch dispatch entries. The historical Device Filter utility could show requests to those devices during a test such as pinging 127.0.0.1. This layer sees transport requests, but it is not a complete view of every packet entering the network stack.
- NDIS intermediate driver. Microsoft provided this driver model for work near the adapter and protocol boundary. Support and installation experience varied across the Windows 9x/ME/NT and 2000/XP releases discussed here; Windows 95 lacked the later intermediate-driver model. Driver signing and user-facing installation requirements also depended on the target release. The period NDIS IM FAQ and DDK cover the supported design. I will instead examine the historical hooking alternative.
- Windows 2000 Filter-Hook Driver. The period DDK describes this filtering option for Windows 2000 and later releases of that family. I will not examine it further here.
- NDIS hooking filter driver. This article examines hooks on selected NDIS functions to track protocol registration and adapter opens, then intercept packet paths. In the original systems, it was comparatively easy to install and could cover dial-up interfaces without some of the extra work required by intermediate drivers. It relied on internal details, however, and was not a supported cross-version driver model.
NDIS Hooking Filter Driver in a Windows 9x/ME
Before continuing, I recommend reading the historical NDIS documentation for these functions:
- NdisRegisterProtocol
- NdisDeregisterProtocol
- NdisOpenAdapter
- NdisCloseAdapter
- NdisSend
Familiarity with Windows 9x/ME virtual device drivers (VxDs) will help with the examples. The fragments use functions and structures from Vireo/Numega VToolsD, which hides some VxD details and keeps the examples compact. Equivalent code could be written using the period DDK.
Let’s start
On Windows 9x/ME, the NDIS library is implemented by the virtual device driver ndis.vxd. To intercept its services, we need our own VxD. I assume the reader already knows how to build one, perhaps with VToolsD; this article concentrates on the NDIS interception.
The hooks must be installed before the protocols call NdisRegisterProtocol during initialization. For this example I chose VNETBIOS_Init_Order. The other VxD parameters depend on the functionality you intend to add.
Hooking of VxD services
VxD service hooking is documented in the period development tools, so I will show only the NdisRegisterProtocol hook. The other four functions need corresponding handlers.
// Function Prototype For Hooking NdisRegisterProtocol
typedef VOID NDIS_API OS_NDIS_REGISTER_PROTOCOL (
PNDIS_STATUS Status,
PNDIS_HANDLE NdisProtocolHandle,
PNDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,
UINT CharacteristicsLength
);
typedef OS_NDIS_REGISTER_PROTOCOL *POS_NDIS_REGISTER_PROTOCOL;...
// Thunks for NDIS services hooking
HDSC_Thunk thunkNdisRegisterProtocolHook;
//...
// Old handlers
POS_NDIS_REGISTER_PROTOCOL OldNdisRegisterProtocol = NULL;
//...
// New NdisRegisterProtocol handler
VOID NDIS_API NH_NdisRegisterProtocol (
PNDIS_STATUS Status,
PNDIS_HANDLE NdisProtocolHandle,
PNDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,
UINT CharacteristicsLength
)
{
//...
}
//...
// Hooking VxD services of NDIS.VXD
NDIS_STATUS HookNdisVxdServices ()
{
OldNdisRegisterProtocol =
(POS_NDIS_REGISTER_PROTOCOL) Hook_Device_Service_C (
GetVxDServiceOrdinal (NdisRegisterProtocol),
NH_NdisRegisterProtocol,
*thunkNdisRegisterProtocolHook
);
if (! OldNdisRegisterProtocol)
return NDIS_STATUS_FAILURE;
//...
return NDIS_STATUS_SUCCESS;
}Call HookNdisVxdServices from the virtual device initialization handler.
What is next?
At this point, intercepting NdisSend lets us observe outgoing traffic, modify it, or pass packets to a user-mode application. For incoming traffic we use the NdisRegisterProtocol hook. A protocol passes an NDIS_PROTOCOL_CHARACTERISTICS structure when it registers; our handler can replace its ReceiveHandler callback. NDIS calls that callback after a miniport indicates received data through NdisIndicateReceive. The adapter may indicate only part of a packet, so obtaining the remainder requires the receive and transfer procedures described in the period DDK. The other registration and adapter hooks let us track which protocols are present and which adapters they open.
Windows ME
Suppose you built the VxD described above. It works on Windows 95/98, but on Windows ME the same VxD-service hooks do not intercept the TCP/IP protocol registration. Windows ME moved TCP/IP to tcpip.sys, which calls NDIS through exports supplied via PELDR_AddExportTable rather than through the VxD services used by this hook. NDIS exports had already supported some miniport configurations, but the protocol path examined here now used them too. To cover that path, the driver must intercept the export-table registration and replace the five relevant function pointers. A second complication is the NDIS_PROTOCOL_CHARACTERISTICS layout: the structure passed by tcpip.sys follows the NT DDK layout, while the ndis.vxd service expects its VxD layout. The exported wrapper must translate between them before calling the VxD service. The Windows ME service also uses an extended, undocumented form of that structure. The original investigation described support for NDIS versions up to 5.0 in this wrapper, while the examined TCP/IP registration used NDIS 4.0. The following definition and pseudocode show the historical approach; their undocumented assumptions are specific to the examined build.
The extended structure is approximately:
typedef struct _ME_NDIS_PROTOCOL_CHARACTERISTICS
{
UCHAR MajorNdisVersion;
UCHAR MinorNdisVersion;
ULONG Reserved;
OPEN_ADAPTER_COMPLETE_HANDLER OpenAdapterCompleteHandler;
CLOSE_ADAPTER_COMPLETE_HANDLER CloseAdapterCompleteHandler;
SEND_COMPLETE_HANDLER SendCompleteHandler;
TRANSFER_DATA_COMPLETE_HANDLER TransferDataCompleteHandler;
RESET_COMPLETE_HANDLER ResetCompleteHandler;
REQUEST_COMPLETE_HANDLER RequestCompleteHandler;
RECEIVE_HANDLER ReceiveHandler;
RECEIVE_COMPLETE_HANDLER ReceiveCompleteHandler;
STATUS_HANDLER StatusHandler;
STATUS_COMPLETE_HANDLER StatusCompleteHandler;
BIND_ADAPTER_HANDLER BindAdapterHandler;
UNBIND_ADAPTER_HANDLER UnbindAdapterHandler;
UNLOAD_PROTOCOL_HANDLER UnloadProtocolHandler;
NDIS_STRING Name;
RECEIVE_PACKET_HANDLER ReceivePacketHandler;
PNP_EVENT_HANDLER PnPEventHandler;
PVOID ReservedHandlers [4];
PVOID CoSendCompleteHandler;
PVOID CoStatusHandler;
PVOID CoReceivePacketHandler;
PVOID CoAfRegisterNotifyHandler;
} ME_NDIS_PROTOCOL_CHARACTERISTICS;The NdisRegisterProtocol wrapper exported through PELDR can be represented by this pseudocode:
// New entry for export from NDIS.VXD exported through PELDR_AddExportTable
VOID NDIS_API
WDM_NdisRegisterProtocol (
OUT PNDIS_STATUS Status,
OUT PNDIS_HANDLE NdisProtocolHandle,
IN PNDIS50_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,
IN UINT CharacteristicsLength
)
{
unsigned short* pUnicode = NULL;
unsigned char* pAnsi = NULL;
unsigned int Counter = 0;
unsigned char* pProto = NULL;
ME_NDIS_PROTOCOL_CHARACTERISTICS MilleniumCharacteristics;
NdisZeroMemory (
&MilleniumCharacteristics,
sizeof (MilleniumCharacteristics)
);
MilleniumCharacteristics.MajorNdisVersion =
ProtocolCharacteristics->MajorNdisVersion;
MilleniumCharacteristics.MinorNdisVersion =
ProtocolCharacteristics->MinorNdisVersion;
MilleniumCharacteristics.Reserved =
ProtocolCharacteristics->Reserved;
MilleniumCharacteristics.OpenAdapterCompleteHandler =
ProtocolCharacteristics->OpenAdapterCompleteHandler;
MilleniumCharacteristics.CloseAdapterCompleteHandler =
ProtocolCharacteristics->CloseAdapterCompleteHandler;
MilleniumCharacteristics.SendCompleteHandler =
ProtocolCharacteristics->SendCompleteHandler;
MilleniumCharacteristics.TransferDataCompleteHandler =
ProtocolCharacteristics->TransferDataCompleteHandler;
MilleniumCharacteristics.ResetCompleteHandler =
ProtocolCharacteristics->ResetCompleteHandler;
MilleniumCharacteristics.RequestCompleteHandler =
ProtocolCharacteristics->RequestCompleteHandler;
MilleniumCharacteristics.ReceiveHandler =
ProtocolCharacteristics->ReceiveHandler;
MilleniumCharacteristics.ReceiveCompleteHandler =
ProtocolCharacteristics->ReceiveCompleteHandler;
MilleniumCharacteristics.StatusHandler =
ProtocolCharacteristics->StatusHandler;
MilleniumCharacteristics.StatusCompleteHandler =
ProtocolCharacteristics->StatusCompleteHandler;
if (ProtocolCharacteristics->Ndis40Chars.Ndis30Chars.MajorNdisVersion > 3)
{
MilleniumCharacteristics.BindAdapterHandler =
ProtocolCharacteristics->BindAdapterHandler;
MilleniumCharacteristics.UnbindAdapterHandler =
ProtocolCharacteristics->UnbindAdapterHandler;
MilleniumCharacteristics.UnloadProtocolHandler =
ProtocolCharacteristics->UnloadHandler;
MilleniumCharacteristics.PnPEventHandler =
ProtocolCharacteristics->PnPEventHandler;
MilleniumCharacteristics.ReceivePacketHandler =
ProtocolCharacteristics->ReceivePacketHandler;
}
if (ProtocolCharacteristics->MajorNdisVersion > 4)
{
MilleniumCharacteristics.CoReceivePacketHandler =
ProtocolCharacteristics->CoReceivePacketHandler;
MilleniumCharacteristics.CoSendCompleteHandler =
ProtocolCharacteristics->CoSendCompleteHandler;
MilleniumCharacteristics.CoStatusHandler =
ProtocolCharacteristics->CoStatusHandler;
MilleniumCharacteristics.CoAfRegisterNotifyHandler =
ProtocolCharacteristics->CoAfRegisterNotifyHandler;
MilleniumCharacteristics.ReservedHandlers [0] =
ProtocolCharacteristics->ReservedHandlers [0];
MilleniumCharacteristics.ReservedHandlers [1] =
ProtocolCharacteristics->ReservedHandlers [1];
MilleniumCharacteristics.ReservedHandlers [2] =
ProtocolCharacteristics->ReservedHandlers [2];
MilleniumCharacteristics.ReservedHandlers [3] =
ProtocolCharacteristics->ReservedHandlers [3];
}
MilleniumCharacteristics.Name.Length =
ProtocolCharacteristics->Name.Length/2;
MilleniumCharacteristics.Name.MaximumLength =
ProtocolCharacteristics->Name.MaximumLength/2;
MilleniumCharacteristics.Name.Buffer =
(PUCHAR) HeapAllocate (
ProtocolCharacteristics->Name.MaximumLength,
0
);
pAnsi = MilleniumCharacteristics.Name.Buffer;
pUnicode = (unsigned short *)
ProtocolCharacteristics->Name.Buffer;
// Copy UNICODE string to ANSI string
for (
Counter = 0;
Counter < MilleniumCharacteristics.Name.Length;
++Counter
)
{
*(pAnsi + Counter) = (UCHAR) *(pUnicode + Counter);
}
// finalize ANSI string with zero
*(pAnsi + MilleniumCharacteristics.Name.Length) = 0;
NH_NdisRegisterProtocol (
Status,
NdisProtocolHandle,
(PNDIS_PROTOCOL_CHARACTERISTICS)&MilleniumCharacteristics,
sizeof (MilleniumCharacteristics)
);
if (*Status == NDIS_STATUS_SUCCESS)
{
// pProto points to internal NDIS structure
// which holds information about protocol
pProto = (PUCHAR) (*NdisProtocolHandle);
// Some internal operation
*(pProto + 0x34) = *(pProto + 0x34) | 1;
}
HeapFree ((PVOID) MilleniumCharacteristics. Name. Buffer, 0);
}That wrapper addressed the Windows ME registration path examined in this investigation.
Next steps
These fragments show how to start investigating such a driver, but they omit many implementation and safety details. The period DDK explains the documented NDIS interfaces; the internal structure layouts and hooks discussed here require separate verification for each target build. Next I will apply a similar approach to Windows NT/2000.
NDIS Hooking Filter Driver in Windows NT/2000
The NT/2000/XP section assumes familiarity with the Windows kernel, kernel-mode driver development and the PE image format.
How to start?
Start with a basic NT kernel-mode driver, using the period DDK or a tool such as DriverWorks. Load order matters: the hook must start after NDIS but before TCP/IP and any other protocol it intends to observe. Because network components load differently on NT and Windows 2000, verify the order on the target system. The historical LoadOrder utility from OSR can help inspect it.
How to hook?
For the NT/2000 implementation, the four functions to intercept are:
- NdisRegisterProtocol
- NdisDeregisterProtocol
- NdisOpenAdapter
- NdisCloseAdapter
Unlike the 9x/ME case, this approach does not hook NdisSend: the relevant call is defined as a macro in the period ndis.h. The four functions above still require interception. One method locates the in-memory ndis.sys image and patches its PE export table; the DDK winnt.h defines the relevant PE structures. This worked in the Windows NT 4.0 setup examined here. On the Windows 2000/XP configurations described in the original research, writing to a protected kernel image could crash the system; page size and write-protection configuration affected the exact failure path. The period techniques below attempted to make that memory writable. They are historical mechanisms, not instructions for current systems:
- Disable kernel-image write protection through the period registry setting EnforceWriteProtection under HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management, with a REG_DWORD value of 0.
- Clear the write-protect bit in CR0 before changing the export table, then restore it afterward. The historical assembly fragment illustrates the idea:
mov ebx, cr0; to receive current state Cr0
push ebx; to save it(him)
and ebx, ~0x10000; to reset WP bats
mov cr0, ebx; to switch off write protection
; Here we modify the table of export....
pop ebx;
mov cr0, ebx; to restore the previous processor state I omit the export-table patch itself because it was not the only historical interception method. Daniel Lanciany instead patched the start of each target function to jump to his handler. The handler restored the original instructions, processed the call, invoked the original function, and installed the patch again. That approach also carried a concurrency risk: another processor could enter the function while its first instructions were being changed. Export-table patching and instruction patching both depend on unsupported implementation details.
What else is necessary to do?
The new NdisRegisterProtocol handler follows the 9x/ME idea: replace incoming-traffic callbacks, including ReceiveHandler and, where relevant, TransferDataCompleteHandler. The protocol arrangement differs, however. In the configuration examined here, the 9x/ME TCP/IP stack registered one protocol that opened both Ethernet adapters and PPPMAC for dial-up Ethernet emulation. On NT/2000/XP, TCPIP.SYS registered TCPIP while WANARP.SYS registered TCPIP_WANARP (RASARP on NT 4.0) and opened the emulated \DEVICE\NDISWANIP interface. The hook must account for both paths.
Much of the remaining work resembles a network protocol driver, as described in the period NT DDK. The difficult part is the new NdisOpenAdapter handler. Its returned NdisBindingHandle points to an internal NDIS_OPEN_BLOCK. Although this structure appeared in ndis.h, its layout was undocumented and could change. It exposes SendHandler and SendPacketsHandler for outgoing traffic, and TransferDataHandler for retrieving received data. The fragment below saves the original binding and substitutes a copy with selected handlers replaced; it is an excerpt, not a complete handler:
// after calling original NdisOpenAdapter
NdisAcquireSpinLock ( &g_OpenAdapterLock );
if (( *Status == NDIS_STATUS_SUCCESS )||
( *Status == NDIS_STATUS_PENDING)
)
{
// Save old binding handle and adapter selected medium
pAdapter -> m_NdisBindingHandle = *NdisBindingHandle;
pAdapter -> m_Medium = MediumArray[*SelectedMediumIndex];
// we does not support adapters except ethernet and \DEVICE\NDISWANIP
if ( !( ( pAdapter -> m_Medium == NdisMediumDix ) ||
( pAdapter -> m_Medium == NdisMedium802_3 )||
( pAdapter -> m_Medium == NdisMediumWan ) ) )
{
// Release already allocated resources
AF_FreeAdapterEntry ( pAdapter );
NdisReleaseSpinLock ( &g_OpenAdapterLock );
return;
}
// Copy Real Open Block to our location
NdisMoveMemory (
&pAdapter -> m_OpenBlock,
*NdisBindingHandle,
sizeof(NDIS_OPEN_BLOCK)
);
// Substitute NDIS_OPEN_BOCK
*NdisBindingHandle = &pAdapter->m_OpenBlock;
// Major work for initializing adapter finished
// Check if complete handler did the remain in case of pending
if ((*Status == NDIS_STATUS_PENDING)&&
(pAdapter->m_dwAdapterState == ADAPTER_STATE_COMPLETE_FAILED)
)
{
// Error while opening adapter occured,
// OpenAdapterComplete left us to free resource
// Release already allocated resources
AF_FreeAdapterEntry ( pAdapter );
NdisReleaseSpinLock ( &g_OpenAdapterLock );
return;
}
// Pointer to our OPEN_BLOCK (for convnience)
pOpenBlock = (PNDIS_OPEN_BLOCK)*NdisBindingHandle;
pAdapter->m_MacBindingHandle = pOpenBlock->MacBindingHandle;
// Substitute Some Real handlers by our version
pAdapter->m_SendHandler = pOpenBlock->SendHandler;
pOpenBlock->SendHandler = OBF_SendHandler;
pAdapter->m_SendPacketsHandler = pOpenBlock->SendPacketsHandler;
pOpenBlock->SendPacketsHandler = OBF_SendPacketsHandler;
pAdapter->m_RequestHandler = pOpenBlock->RequestHandler;
pOpenBlock->RequestHandler = OBF_RequestHandler;
pAdapter->m_TransferDataHandler = pOpenBlock->TransferDataHandler;
What do we have by now?
Even a minimal driver can be compact in source, but the hooking logic is delicate. In the original NT/2000 setup, installation also required several registry entries.
The static hooks above must be installed early, before protocol registration, and the driver described here is not designed to unload afterward. A historical alternative patched structures belonging to protocols that were already registered. It began by registering a dummy protocol to obtain an NdisProtocolHandle, then followed the internal NDIS_PROTOCOL_BLOCK list. Those blocks exposed protocol callbacks and adapter bindings through NDIS_PROTOCOL_CHARACTERISTICS and NDIS_OPEN_BLOCK structures. The dummy protocol could use a receive callback that returned NDIS_STATUS_NOT_ACCEPTED. Once the handle exposed the protocol-block chain, the driver could inspect NDIS_PROTOCOL_CHARACTERISTICS callbacks and each NDIS_OPEN_BLOCK’s Send, SendPackets and Request handlers, then replace selected handlers as it traversed the list. This method was described for 9x/ME and NT/2000/XP, although the driver form and internal layouts differed. The original investigation also associated this style of dynamic filtering with the contemporary ZoneAlarm/TrueVector product; that attribution is historical. Changing live kernel structures requires synchronization and exact knowledge of the target build, so apparent simplicity should not be mistaken for safety.
Conclusion
I have surveyed the main filtering approaches considered at the time and examined NDIS hooking in more detail. On the systems tested, it offered broad visibility into protocol registration and traffic, which made it useful for the original investigation. These fragments are neither a complete driver nor a supported design for modern Windows. For current drivers, consult the supported NDIS filter and WFP interfaces for the target system. The period DDK remains useful for its documented interfaces, but it does not document all the internal structures and hooks used here.