NDIS MONITOR .NET 32-bit v1.00 for Windows 2K, XP, 2K3
NDIS Monitor is a rather complex example application written (for the front-end) with the Microsoft .NET WinForms technology that allows to catch and log the exchange of packets data between NDIS miniport drivers and network protocol modules that occurs in kernel space.
Check out my NDIS Monitor's article on The Code Project here, for a more simplified description of how to use the program. |
It is a kind of simple but extremely extensible packet analyzer with the unique feature of being able to intercept loopback exchange of packets that occurs to and from the Microsoft Loopback Adapter (more on this later). Also with it you can monitor the NDIS action of WAN Adapters (like ADSL Modems, for example). It doesn't require any kind of installation: run it, select which miniport to hook and start seeing packets as they are sent and received from the network. It demonstrates also a bunch of advanced .NET features at work used in a real-world application: Multithreading, Message Filtering, Reflection, CodeDom, WinForm UserControls, PInvoke, Marshaling from the unmanaged space, Synchronization through Windows Messages etc. The source code is fully commented.
FEATURES
|
Compatible with 32 bit versions of Windows 2000, XP, 2003. |
|
|
Hooking of Ethernet Adapters in order to show exchange of packets that occurs with corresponing NDIS Protocols. |
|
|
Hooking of Protocols such as "TCPIP_WANARP" gives access to the packets sent to and received from devices such as ADSL Modems. |
|
|
Integrated and extensible packet analyzer. |
|
|
Special NAT feature that allows the monitoring of the data sent to and received from the Microsoft Loopback Adapter. |
|
|
Open and Save features of a packet log in a proprietary file format (".nmc" files). |
|
|
Integrated C# script window and CodeDom compiler that allows the writing of filters and analyzers of the packet data on the fly. |
|
|
Integrated output window that allows the log of actions and messages by the script code. |
|
|
Object model for representing received packet data for allowing the writing of more abstract script code. |
|
|
Automatic saving of the script code. |
|
SCREENSHOTS
These are other screenshots of the application at work:
INTRODUCTION
Some time ago, I had the opportunity to write a tunneling application that had the requirement of catching and redirecting TCP/IP communication over several TCP and UDP ports to a single TCP port for transmission via the World Wide Web. In that occasion I conducted a very extensive research about the topic/problem and evaluated a bunch of several, possible solutions. One of the requirement of the project was that the resulting technology had to be consumed without any kind of previous installation: the end user had to be saved from the bother of rebooting his machine, for example. In that occasion, I preferred a completely user mode solution: I wrote an hook for several WinSock APIs and was able to intercept the send and receive requests of a well-known application executable in order to forward the hooked stream of data to a TCP-connected peer in another part of the network. Among the various technologies and solutions I researched one in particular attracted my attention because of its flexibility and uniformity: NDIS miniport driver APIs hooking. As I discovered later the base approach of the technique I implemented in the kernel driver that NDIS Monitor calls in order to obtain packet data and statistics from kernel space is well-known, rather documented and even implemented in commercial-class products such as personal/corporate firewalls and VPN servers.
TWO WORDS ABOUT THE NDIS HOOK METHOD
NDIS Miniport drivers are special device drivers that the Operating System loads in order to control the NIC card(s) that are installed in your system. One of the main purposes of this type of drivers is sending and receiving data through the NIC card: then, this data, represented in the form of Packets, can be forwarded to the corresponding higher level driver(s) that are bound to the Miniport Driver itself. Generally these "higher level drivers" are transport protocol drivers, such as the "TCPIP" one that you can see in the screenshot above. The NDIS library sits in between the Miniport driver and the Transport driver for managing the interaction between the two components. The binding between the protocol driver and the miniport driver is generally set up by the NDIS library when the protocol driver itself calls the "NdisOpenAdapter" API.
The hooking system described in this article and implemented in the NDIS Monitor application itself leverages this architecture and is aimed to intercept the exchange of packet data that occurs between an arbitrary NDIS Miniport driver and its bound NDIS Protocol module.
In this specific case, the approach to the hook problem consists in the replacement of several function pointers in two proprietary and so-so documented NDIS structures: NDIS_OPEN_BLOCK and NDIS_MINIPORT_BLOCK. The former structure represents a binding between a Miniport and a Protocol driver, while the latter structure contains various informations that are specific to every registered Miniport driver in a system. The solution described here is in part made possible by the fact that a bunch of fundamental NDIS APIs are implemented as macros in the corresponding NDIS header files (such as the "NdisMTransferDataComplete" API). For the sake of precision, when compiling miniport drivers for the Intel x86 Architecture, for binary compatibility reasons between Windows 98/ME and Windows NT-based systems, several NDIS APIs are declared as static functions (imported from NDIS.SYS) instead of macros that directly reference NDIS structure's fields. The not-compiled, macro versions of the prototypes of these functions (selectively excluded from x86 compilations) constitute however an important indication about the mapping between a NDIS API and its corresponding field in a NDIS native structure. You can check it out by yourself looking at the main NDIS header file available in the standard DDK installation (NDIS.H) and searching for API names such as "NdisSend"...
Specifically, for our hook purposes, we need to get a peek at the parameters of the following NDIS functions as they are called by the involved players:
|
NdisSend function: this function is called by a protocol driver whenever there is a packet to send through the specified adapter. Actually the NdisBindingHandle parameter is a pointer to the corresponding NDIS_OPEN_BLOCK structure whose "SendHandler" field points to the actual Miniport implementation (as can be argued looking at the macro version of the declaration of this function). Hooking this function can enable the hooker to get a peek at the NDIS_PACKET structure that represents the outgoing packet that the protocol wants to send through the network. NDIS Monitor job is collecting these packets, converting them in linear-memory allocations and storing them in an opportunely synchronized circular buffer ready to be retrieved by a user mode application upon the reception of a special IOCTL request, in our case. A firewall application can, for example, reject this function invocation and not to call the original miniport-implemented function, not allowing the packet to reach the wire. A VPN application can reroute this packet (encapsulating it as needed) and (eventually) forward it to another NIC Miniport, for example. The prototype is:
VOID
NdisSend(
OUT PNDIS_STATUS Status,
IN NDIS_HANDLE NdisBindingHandle,
IN PNDIS_PACKET Packet
);
|
|
|
ProtocolReceive function as specified in the "ReceiveHandler" and "PostNt31ReceiveHandler" fields of the NDIS_OPEN_BLOCK structure. This function (implemented by a protocol driver) is called by NDIS whenever a function such as "NdisMIndicateReceivePacket" is called by a bound Miniport driver in the case a new packet arrives from the network. The job of the NDIS Monitor application is the same as in the NdisSend case: the packet is simply stored in a circular buffer, waiting to be fetched by the user mode application. The prototype is:
NDIS_STATUS
ProtocolReceive(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_HANDLE MacReceiveContext,
IN PVOID HeaderBuffer,
IN UINT HeaderBufferSize,
IN PVOID LookAheadBuffer,
IN UINT LookaheadBufferSize,
IN UINT PacketSize
);
|
|
|
NdisSendPackets function as specified in the "SendPacketsHandler" field of the NDIS_OPEN_BLOCK structure: this function is called by a protocol driver whenever there is an array of packets to send through the specified adapter. The prototype is:
VOID
NdisSendPackets(
IN NDIS_HANDLE NdisBindingHandle,
IN PPNDIS_PACKET PacketArray,
IN UINT NumberOfPackets
);
|
|
|
ProtocolReceivePacket function as specified in the "ReceivePacketHandler" field of the NDIS_OPEN_BLOCK structure: this function is implemented by a Protocol driver that wants to be notified when a new packet arrives from the network. The prototype is:
INT
ProtocolReceivePacket(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET Packet
);
|
|
|
NdisTransferData function as specified in the "TransferDataHandler" field of the NDIS_OPEN_BLOCK structure: this function is tipically called in the context of a "ProtocolReceive" invocation in order to obtain the remaining bytes of a received packet. NDIS Monitor hooks this function in order to avoid to call the original function directly in its own hook of "ProtocolReceive": infact this function (if the received packet is of interest to the target protocol) is already called by the protocol driver itself and the documentation advises not to call NdisTransferData more that once from the same protocol. NDIS Monitor intercepts the invocation and parameter data of this function when called by the underlying protocol and then integrates the resulting packet data to the Media header and Lookaheader buffer bytes previously passed to the "ProtocolReceive" function. The prototype is:
VOID
NdisTransferData(
OUT PNDIS_STATUS Status,
IN NDIS_HANDLE NdisBindingHandle,
IN NDIS_HANDLE MacReceiveContext,
IN UINT ByteOffset,
IN UINT BytesToTransfer,
IN OUT PNDIS_PACKET Packet,
OUT PUINT BytesTransferred
);
|
|
|
NdisMTransferDataComplete function as specified in the "TDCompleteHandler" field of the NDIS_MINIPORT_BLOCK structure. This function is called by a Miniport driver whenever a "NdisTransferData" invocation that returned NDIS_STATUS_PENDING completes. NDIS Monitor intercepts the invocation of this function and puts the returned packet data in the circular buffer. The prototype is:
VOID
NdisMTransferDataComplete(
IN NDIS_HANDLE MiniportAdapterHandle,
IN PNDIS_PACKET Packet,
IN NDIS_STATUS Status,
IN UINT BytesTransferred
);
|
|
But now a legitimate question arises: how to get a pointer to the aforementioned NDIS_OPEN_BLOCK and NDIS_MINIPORT_BLOCK structures? Remember that the NDIS_OPEN_BLOCK structure represents a binding between a Protocol driver and a Miniport diver. However, the answer is pretty simple if you look at the definitions of the structures themselves: the NDIS_OPEN_BLOCK structures allocated for a given protocol in response to "NdisOpenAdapter" invocations are organized in kernel memory as a linked list of elements and iterating through that list is simply a matter of reading a pointer in a loop. A more interesting problem is posed when you want to obtain a pointer to the head item of the linked list itself: the solution I have studied involves the creation of a fake protocol for obtaining a pointer to a NDIS_PROTOCOL_BLOCK structure that the system allocates in response to a NdisRegisterProtocol invocation. Furthermore, as expected, also the protocol structures are organized in memory as a linked list. Now, as you can guess, the head item of the NDIS_OPEN_BLOCK linked list we are searching for is pointed by a field of that NDIS_PROTOCOL_BLOCK. The structure of the HookInstalledProtocols function that I have implemented in my driver is roughly as follows:
__try
{
hProtocol = hFakeProtocolHandle;
while( hProtocol = GetNextProtocol( hProtocol ) )
{
pobAdapter = GetOpenQueue( hProtocol );
if ( pobAdapter )
{
do
{
...
} while( pobAdapter = GetNextOpen( pobAdapter ) );
}
}
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
...
}
I have omitted about 500 lines of code but the source above should give you an enough complete idea of what happens when the driver receives the IOCTL_VPCKNT_INITIALIZE_HOOK IRP. The idea of creating a fake protocol in order to get a pointer to a first element (of type NDIS_PROTOCOL_BLOCK) that would initiate the whole startup iteration, as I discovered later, is not unknown to NDIS experts around the web. Furthermore it seems that more that one commercial-class product is based on this trick for imposing their network logic to the host system.
Before finishing this talk about the NDIS hooking aspects of NDIS Monitor, I suppose it may be interesting spending two words about the call dispatching mechanism itself that reroutes NDIS function calls to our hook procedures. Fundamentally each function pointer stored in the aforementioned NDIS native structures is replaced by a pointer to the following structure, allocated in the NonPagedPool space of kernel memory (so satisfying the potential higher-level IRQL demands of a NDIS caller and, in the upcoming 64-bit version of NDIS Monitor, preventing Data Execution Protection BugChecks on Windows XP SP2):
#pragma pack(push, 1)
typedef struct _NDISHOOK_HANDLER_STUB
{
BYTE bPushImm32Opcode; // It is 0x68.
DWORD dwOperationID; // Identifier of the stub.
BYTE bJmpRel32Opcode; // It is 0xE9.
DWORD dwJmpDisplacement; // Relative address.
BYTE bPad[ 0x6 ]; // PAD: 0, 0, 0, 0, 0, 0
} NDISHOOK_HANDLER_STUB, *PNDISHOOK_HANDLER_STUB;
#pragma pack(pop)
This structure, when filled with the correct data, will reroute the processor execution to the following assembler function (however first pushing the identifier of the stub itself onto the stack):
static PVOID g_vpvHandlersVector[] =
{
& New_SendHandler, // SENDHANDLER_FNID
& New_ReceiveHandler, // RECEIVEHANDLER_FNID
& New_PostNt31ReceiveHandler, // POSTNT31RECEIVEHANDLER_FNID
& New_SendPacketsHandler, // SENDPACKETSHANDLER_FNID
& New_TransferDataHandler, // TRANSFERDATAHANDLER_FNID
& New_ReceivePacketHandler, // RECEIVEPACKETHANDLER_FNID
& New_TDCompleteHandler // TDCOMPLETEHANDLER_FNID
};
static void __declspec(naked) HandlerGeneralDispatcher ( void )
{
__asm
{
// ### Load in EAX and ECX the Code found in the Stack. ###
mov eax, dword ptr[ esp ] // eax = Code: 0xYYYYZZZZ. (y = Adapter Ordinal, z = Handler Ordinal.)
mov ecx, eax // ecx = Code: 0xYYYYZZZZ. |