Windows网络驱动、NDIS驱动(微端口驱动、中间层驱动、协议驱动)、TDI驱动(网络传输层过滤)、WFP(Windows Filtering Platform)

catalog

0. 引言
1. Windows 2000网络结构和OSI模型
2. NDIS驱动
3. NDIS微端口驱动编程实例
4. NDIS中间层驱动编程实例
5. NDIS协议层驱动编程实例
6. TDI驱动
7. TDI的过滤框架
8. WFP(Windows Filtering Platform windows过滤平台)

 

0. 引言

最早出现的网络驱动应该是网卡驱动,这是Windows的下进行网络安全攻防常见的需求,为了进一步分割应用程序的网络数据传输与下层协议直到下层硬件的关系,又出现了协议驱动,后来微软和硬件商联合制定了NDIS标准,作为从硬件到协议的内核驱动程序的调用接口标准,而协议驱动与应用层的API之间,则出现了TDI接口,即从上到下的关系是

应用层API -> TDI -> 协议驱动 -> NDIS -> 下层硬件

0x1: 网卡驱动

网卡驱动程序(Device Driver)全称为"设备驱动程序",是一种可以使计算机中央处理器: CPU控制和使用设备的特殊程序,相当于硬件的接口,操作系统通过这个接口,控制硬件设备的工作。所有的硬件都要安装驱动程序,没有驱动程序的硬件是运行不了的,就像一辆有轮胎但是没有传动轴的汽车一样跑不起来,控制不了
假如某设备的驱动程序未能正确安装,便不能正常工作。 网卡驱动程序就是CPU控制和使用网卡的程序
网卡驱动的问题在于它总是一个整体的形式存在,无法提供灵活的Hook、Filter、Callback接口,无法实现除了操作硬件之外更高层次的数据处理需求

0x2: 上层协议驱动

上层协议驱动应用于TDI接口或其它向用户提供服务的特定应用接口。例如驱动调用NDIS分配包,向包中拷贝数据和向底层发送包。它也在它的底层提供协议接口,来接收下层驱动发送来的包

0x3: 中间协议驱动

中间协议驱动接口位于上层协议驱动和微端口驱动之间。对于上层传输驱动程序来书,中间驱动看起来像是微端口驱动。对微端口驱动来说,看起来像是协议驱动。使用中间协议驱动的主要是为了传输媒质,存在于对于传输驱动未知和微端口管理之间的新的媒质类型

0x4: 基于WDK的windows内核驱动入门编程

首先需要下载WDK(Windows Driver Kit),WDK已经自带所有需要的头文件、库、C/C++语言及汇编语言的编译器、链接器。需要注意的是,因为这并不是应用程序编程,所以所有的Win32 API函数都不能使用,部分C Runtime函数也不能使用,但是文档中有说明记录的函数都可以使用
first.c

#include <ntddk.h> 

//提供Unload函数,让驱动程序能动态卸载,方便调试
NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("DriverUnload\n"); 
    return STATUS_SUCCESS;
}

//DriverEntry入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)    
{
    DbgPrint("Hello World\n"); 

    //设置一个卸载函数,便于这个函数退出
    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}  

makefile

!IF 0
Copyright (C) Microsoft Corporation, 1999 - 2002
Module Name:
    makefile.
Notes:
    DO NOT EDIT THIS FILE!!!  Edit .\sources. if you want to add a new source
    file to this component.  This file merely indirects to the real make file
    that is shared by all the components of Windows NT (DDK)
!ENDIF
!INCLUDE $(NTMAKEENV)\makefile.def

SOURCES

TARGETNAME=first 
TARGETTYPE=DRIVER 
SOURCES=first.c

菜单->windows driver kits->wdk版本->build environment->windows server 2003->launch windows->x64 Checked Build Environment
pushd C:\Documents and Settings\Administrator\桌面\网络驱动编程\first
build

net start first

Relevant Link:

http://book.51cto.com/art/200905/125691.htm
http://www.installsetupconfig.com/win32programming/windowsdriverkitswdk15_6.html
http://blog.chinalxnet.com/?uid-2-action-viewspace-itemid-26
http://guides.wmlcloud.com/technology/windows-7---kernel-mode-installation-and-build---wdk-build-tools,-build-environment,-building-a-project-.aspx 

 

1. Windows 2000网络结构和OSI模型

Windows 2000网络结构是以国际标准化组织(ISO)制定的七层网络模型为基础的,1978年,ISO制定的开放式系统(OSI)参考模型,将网络描述为一系列的协议层,在每个协议层中完成一系列的特定功能。每一层都向上一层提供明确的服务,同时将本层服务的实现封装起来。一个在相邻层之间完善的接口定义了下层对上层所提供的服务以及如何访问这些服务(应用层、表示层、会话层、传输层、网络层、链路层、物理层、物理介质层)
Windows 2000的网络驱动程序实现了这个网络结构的下面四层

0x1: 物理层

这是OSI模型中的最底层,这一层用来通过物理介质接收和发送的原始的没有结构的二进制数据流,它描述了对于物理介质的电/光,机械和功能的的接口,物理层为所有的上层传送信号,在Windows 2000中,物理层通过网络接口卡(NIC)来实现,物理层的收发是依缚于NIC介质的。使用串行端口的网络组件,物理层也同样包含了底层的网络软件,它定义了串行位流是如何分成数据包的

0x2: 数据链路层

电气电子工程师协会(IEEE)将该层进一步分成了两个子层,LLC和MAC

1. LLC子层(逻辑链路控制子层)用于将数据帧从一个结点无错的传输到另一个结点。LLC子层用来建立和终止逻辑链接,控制帧流,对帧排序,接收帧,并且对没有被接收的帧进行重发。LLC子层使用帧应答和帧的重发为经过链路层的上层提供了真正的无错发送,在Windows 2000网络结构中,逻辑链路控制子层在传输驱动程序中实现
2. MAC子层(介质访问控制子层)控制物理介质的访问,检查帧的错误,并且管理接收帧的地址认证,介质访问控制子层则在网络接口卡(NIC)中实现,NIC由一个名为NIC驱动程序的设备驱动程序软件控制。Windows 2000的附带了许多常用的NIC的驱动程序

0x3: 网络层

这一层控制着子网的运作,它基于以下因素决定数据的物理路径

1. 网络状况
2. 服务优先权
3. 其他因素,包括路由、流量控制、帧的分解和重组、逻辑到物理地址的映射、用户帐号

0x4: 传输层

这一层确保信息传送的无错传输,连续传输和不丢失或不重复。它使得上层协议与上层协议之间或与它同层的协议之间通讯不必关心数据的传输。传输层所在的协议栈至少应包括一个可靠的网络层,或在逻辑链路控制子层中提供一个虚电路。例如,因为Windows 2000 NetBEUI传输驱动程序包括一个与OSI兼容的LLC子层,它的传输层的功能就很小。如果协议栈不包括LLC子层,并且网络层不可靠,并且/或者支持自带地址信息(例如TCP/IP的IP层或NWLINK的IPX层),那么传输层应能进行帧的顺序控制和帧的响应,同时要对未响应帧进行重发
在Windows 2000网络结构中,逻辑链路层,物理层和传输层都是通过名为传输驱动程序的软件实现的,它有时也称作协议,协议驱动程序或协议模块。Windows 2000附带了TCP/IP,IPX/SPX,NetBEUI和AppleTalk传输驱动程序

 

2. NDIS驱动

0x1: 简介

NDIS是Network Driver Interface Specification,即网络驱动接口规范。NDIS的主要目的就 是为NIC(网络接口卡,Network Interface Cards)制定出标准的API接口。MAC(介质访问控制,Media Access Controller)设备驱动封装了所有的NIC硬件 实现,这样一来所有的使用相同介质的NIC就可以通过通用的编程接口被访问
NDIS同时也提供一个函数库(有时也称作wrapper),这个库中的函数可以被MAC驱动调用,也可以被高层的协议(例如TCP/IP)驱动调用。这些wrapper函数使得MAC驱动和协议驱动的开发变得更加容易
NDIS(Network Driver Interface Specification)是网络驱动程序接口规范的简称。它横跨传输层、网络层和数据链路层,定义了网卡或网卡驱动程序与上层协议驱动程序之间的通信接口规范,屏蔽了底层物理硬件的不同,使上层的协议驱动程序可以和底层任何型号的网卡通信。
NDIS为网络驱动程序创建了一个完整的开发环境,只需调用NDIS函数,而不用考虑操作系统的内核以及与其他驱动程序的接口问题,从而使得网络驱动程序可以从与操作系统的复杂通讯中分离,极大地方便了网络驱动程序的编写。另外,利用NDIS的封装特性,可以专注于一层驱动的设计,减少了设计的复杂性,同时易于扩展驱动程序栈

NDIS支持三种类型的网络驱动程序

1. 网卡驱动程序(NICdrivers): 微端口驱动程序
网卡驱动程序是网卡与上层驱动程序通信的接口,它负责接收来自上层的数据包,或将数据包发送到上层相应的驱动程序,同时它还完成处理中断等工作 

2. 中间驱动程序(InterMediateProtocolDrivers)
中间驱动程序位于网卡驱动程序和协议驱动程序之间,它向上提供小端口(Minport)函数集,向下提供协议(protocol)函数集
    1) 因此对于上层驱动程序而言,它是小端口驱动程序
    2) 对于底层的驱动程序,它是协议驱动程序

3. 协议驱动程序(Upper Level Protocol Drivers)
协议驱动程序执行具体的网络协议(传输层),如IPX/SPX、TCP/IP等。协议驱动程序为应用层客户程序提供服务,接收来自网卡或中间驱动程序的信息 

自上而下的关系为

上层驱动程序(协议驱动程序) -> 中间驱动程序 -> 网卡驱动程序(NICdrivers) -> 网卡

为了更好地支持扩展性,网络驱动模型的每一层都定义了对外公开的公共接口,与它相连接的上层或者下层驱动模型不必关心它的内核如何实现,而只要根据它所提供的公共接口完成相应的工作即可,各个层级之间按照预设好的接口上下连接,协同工作,紧密相连
NDIS定义了网卡或网卡驱动程序与上层协议驱动程序之间的通信接口规范,它屏蔽了底层物理硬件的不同,使上层的协议驱动程序可和底层任何型号的网卡通信

0x2: 网卡驱动程序(NICdrivers): 微端口驱动程序

一个 NDIS 微端口驱动程序(也叫微端口NIC 驱动程序)有两个基本功能

1. 管理一个网络接口卡(NIC),包括通过NIC 发送和接收数据 
2. 与高层驱动程序相接,例如中间层驱动程序和传输协议驱动程序 

一个微端口驱动程序与它的NIC通信,并且通过NDIS库与高层驱动程序通信。NDIS库对外提供了一整套的函数(NdisXXX 函数),这些函数封装了微端口需要调用的所有操作系统函数,同时,微端口必须向外提供一组入口(MiniportXxx函数),使NDIS可以为了完成自己或高层驱动程序的任务而访问微端口

0x3: 中间驱动程序(InterMediateProtocolDrivers)

中间层驱动程序一般位于微端口驱动程序和传输协议驱动程序之间,因为它在驱动程序层结构的中间位置,所以既与上层协议驱动程序通信又要与下层微端口驱动程序通信,它是一种混合型驱动程序,它把NDIS协议驱动和NDIS小端口驱动的功能结合在一起

1. 在它的下界,中间层驱动程序提供了协议入口点(ProtocolXxx函数),NDIS调用这些函数传递下层微端口的请求。对于一个下层微端口驱动程序,一个中间层驱动程序这时就仿佛是一个
协议驱动程序
2. 在它的上界,中间层驱动程序提供了微端口的入口指针(MiniportXxx函数),一个或多个上层协议驱动程序通过NDIS调用这些函数进行通信。对于上层协议驱动程序,一个中间层驱动程序这时就仿佛是一个微端口驱动程序

对NDIS协议驱动而言,它展示NDIS小端口的特征,让NDIS协议驱动以为自己是小端口驱动而绑定它,对下层的NDIS小端口驱动而言,它展示协议驱动的特征,自己绑定在小端口驱动上(在windows内核网络架构中,协议驱动是主动绑定小端口驱动的)
从理论上来讲,中间层驱动的个数是没有限制的,可以不断叠加(这也是网络防火墙兼容的架构基础),如果在windows本身的协议驱动和小端口驱动之间,插入了一个特制的NDIS中间层驱动,所有NDIS协议驱动和NDIS小端口之间的交互活动,都会经过这个中间层过滤,这样我们就能捕获这些事件进行处理 

0x4: 协议驱动程序(Upper Level Protocol Drivers)

一个网络协议在NDIS驱动程序层次结构中属于最高层驱动程序,而它经常在实现传输层协议的传输驱动程序中被用作最底层的驱动程序,例如TCP/IP 或IPX/SPX

1. 一个传输协议驱动程序分配包,从应用程序中将数据拷贝到包中,并且通过调用NDIS函数将这些包发送到低层驱动程序中
2. 协议驱动程序也为从下层驱动程序中接收包提供了接口
3. 一个传输协议驱动程序将接收到的数据转换成相应的客户应用数据,在它的下层,协议驱动程序与中层网络驱动程序和微端口NIC驱动程序相连接
    1) 协议驱动程序调用NdisXxx函数来发送包,读取和设置由低层驱动程序所维护的信息,以及使用操作系统服务(NDIS代表下层驱动程序调用这些函数为自己或向上指示接收包、指示下层驱动程序的状态、以及与协议驱动程序进行通信)
    2) 协议驱动程序也提供了一套入口点(ProtocolXxx函数)(对于上层,传输协议驱动程序对高层驱动程序提供了一个私有接口)

0x5: NDIS协议驱动和NDIS中间层驱动的区别及其应用场景

中间层驱动在信息安全领域的应用和协议驱动的应用容易产生混淆

1. NDIS协议驱动和NDIS中间层驱动都能截获"所有本机能接收到的包"
2. 编写一个小型的协议驱动,可以截获本机接收到的所有的包,这并不是通过拦截,而是注册一个协议,并绑定了所有下层可以收包的小端口驱动的缘故,被绑定的小端口驱动总是向绑定者(协议驱动)提交网卡接收到的数据包(中断),但是一个应用程序用TCP协议发送数据时,显然不会通过我们注册的新协议,而是调用TCP/IP协议,这样,我们注册的NDIS协议驱动自然没有机会能拦截到上层应用的发包过程,因此,NDIS协议驱动在安全领域,只适合用来做发包工具、嗅探器(拦截本机接收到的包),而不适合做防火墙(因为防火墙需要双向的实时拦截)
3. 中间层驱动不仅绑定了所有小端口驱动,而且还"所有协议驱动所绑定"(因为它承上启下的特性),于是中间层驱动在理论上可以做到拦截所有通过NDIS发送和接收的数据包,无论应用程序使用什么协议都无法绕过,因此适合做包过滤防火墙的核心组件
//能否截获所有本机发出去的包,成为了NDIS协议驱动和NDIS中间层驱动在信息安全领域的应用方面最本质的区别
4. 在NDIS中间层驱动中,对接收和发送的数据包,可以采用的处理方法几乎是无限的;可以接受、拒绝、修改,对于大部分安全软件来说,是采用规则过滤的方式,即用一系列的规则对这个数据包进行考核,对符合规则合法的包让它通过;对不符合规则的攻击包/无用的包直接丢弃,同时也有一些安全软件对以太网包进行加密/解密的工作

0x6: NDIS网络驱动程序编程要点

1. 可移植性
    1) NDIS驱动程序应很容易在Windows的平台间移植。一般说来,从一个硬件平台移植到另一个平台只需要将它在兼容系统的编译中重新编译即何
    2) 驱动程序开发者应当避免调用与操作系统相关的函数,因为这将使他们的驱动程序不可移植。应用NDIS函数替换这些调用,只调用NDIS函数将使代码在支持NDIS的微软操作系统中可移植,NDIS为编写驱动程序提供了很多支持函数,并且直接调用操作系统是不需要的 
    3) 驱动程序应用C来编写。更近一步,驱动程序代码应当限制在ANSI C标准并且应当避免使用不被其他兼容系统编译器支持的语言特性。驱动程序代码编写不应当使用任何ANSI C标准指明为"implementation defined"的特性来编写 
    4) 驱动程序应当避免使用任何在不同平台上大小和结构变化的数据类型,驱动程序代码不应该调用任何C的运行时库函数,而是限于调用NDIS提供的函数 
    5) 在内核模式下漂移指针是不允许的。试图运行这样的操作将是一个致命的错误 
    6) 如果驱动程序代码要支持特殊平台的特性,那么这些代码应当包含在#ifdef和#endif声明之间 

2. 多处理器支持
    1) 编写的代码应当可以在多处理器系统中安全运行。这对于编写可移植的Windows驱动程序是很重要的。一个网络驱动程序必须使用NDIS函数库提供的多处理器安全保证
    2) 在单处理器环境下,在一个时刻单处理器只运行一条机器指令,既使这样,当包到达或时间中断发生时,对于NIC或其他设备执行的中断也可能发生。典型的,当正在操纵数据结构时,例如它的队列时,驱动程序对NIC发出停用中断来操纵数据,随后再发生可用中断。许多线程在单处理器环境下表现的好像是在同一时刻运行的,但是实际上它们却是在独立的时间片上运行的 
    3) 在多处理器环境下,处理器同时可运行多条机器指令,一个驱动程序必须异步化,这使得当一个驱动程序函数正在操纵一个数据结构时,同样的在其他处理器运行的驱动程序函数不能修改共享的数据。在一个SMP机器中,所有的驱动程序代码都要重新装入,为了消除这种资源保护问题,Windows驱动程序使用自旋锁

3. IRQL
所有NDIS调用的驱动程序函数都运行在系统决定的IRQL下,PASSIVE_LEVEL < DLSPATCH_LEVEL<DIRQL中的一个。例如
    1) 一个微端口初始化函数、挂起函数、重启函数和有时的关闭函数通常都运行在PASSIVE_LEVEL下
    2) 中断代码运行在DIRQL下
所以NDIS中层或协议驱动程序从不运行在DIRQL下。所有其他的NDIS驱动程序函数运行在IRQL <= DISPATCH_LEVEL下 
驱动程序函数运行于的IRQL将影响调用什么样的NDIS函数。特定的函数只可在IRQL PASSIVE_LEVEL下调用,其他的函数可在DISPATCH_LEVEL或更低层调用。一个驱动程序的编写者应当检查每一个NDIS函数的IRQL限制 
任何与驱动程序的ISR共享资源的驱动程序函数必须能将它的IRQL升级到DTRQL来防止争用情况的发生,NDIS提供了这种机质 

4. 同步和指示
当两个线程共享可被同时访问的资源时,无论是单处理机还是SMP,同步是必须的。例如,对于一个单处理机,如果一个驱动程序正在访问一个共享资源时,被一个运行在更高IRQL(例如ISR)的函数中断时,必须保护共享资源以阻止这种争用的发生而使资源处于不确定状态
在一个SMP中,两个线程可以在同一时刻运行,在不同处理器上并且试图来修改同一数据的访问必须同步。
NDIS提供了自旋锁可以用来对在同一IRQL下运行的线程间访问共享资源实现同步。当两个线程在不同IRQL下访问共享资源时,NDIS提供了一种机制来临时提高低IRQL代码的IRQL,以使得访问共享资源串行化 
NDIS提供下面四种机制来保证同步:

4.1 自旋锁
自旋锁提供了一个用来保护共享资源的同步机制,这种资源是单处理器或一个多处理机下的、运行在IRQL > PASSIVE_LEVEL下的、内核模式中的线程所共享使用的。一个自旋锁在同时运行在一个SMP机上不同的执行线程之间提供同步。一个线程在访问保护资源前获得一个自旋锁。自旋锁使得任务线程中只有持有自旋锁的线程可使用资源。一个等待自旋锁的线程将在试图获得锁时间内循环,直到持有锁的线程释放为止 
自旋锁还存在着一个不太明显但很重要的事实:你仅能在低于或等于DISPATCH_LEVEL级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到DISPATCH_LEVEL级上运行。在内部,内核能在高于DISPATCH_LEVEL的级上获取自旋锁,但你和我都做不到这一点。当KeAcquireSpinLock获取自旋锁时,它会把IRQL提升到DISPATCH_LEVEL级上。当KeReleaseSpinLock释放自旋锁时,它会把IRQL降低到原来的IRQL级上。如果你知道代码已经处在DISPATCH_LEVEL级上,你可以调用两个专用函数来获取自旋锁。KeAcquireSpinLockAtDpcLevel及 KeReleaseSpinLockFromDpcLevel。一个编写很好的网络驱动程序应该会减少自旋锁持有的时间
一个典型的使用自旋锁的例子是保护一个队列。例如,微端口发送函数MiniportSend将协议驱动程序传来的包进行排队。因为其他驱动程序函数也使用这个队列,MiniportSend必须用一个自旋锁保护这个队列使得在一个时刻只有一个线程可操纵这个队列。Miniport Send获得自旋锁,添加包到队列后释放自旋锁。使用自旋锁保证持锁线程是唯一修改队列的线程,同时使得包被安全地添加到队列中。当NIC驱动程序从队列中取走包时,通过同样的自旋锁保护这个访问。当执行指令修改队列头或任何队列组成域时,驱动程序必须用自旋锁保护队列

4.2 避免死锁问题
Windows并不限制网络驱动程序同时持有多于一个的自旋锁。但是,驱动程序的某部分在持有自旋锁B时,试图获得自旋锁A,并且其他部分在持有锁A时,试图获得自旋锁B时,死锁就会发生。如果要获得多于一个的自旋锁,驱动程序应当通过强制以某一顺序获得锁来避免死锁,这就是说,如果一个驱动程序强制在获得自旋锁A之后才可获得锁B,那么上述情况就不会发生
总得来说,使用自旋锁将对系统性能带来负面效应,所以驱动程序不应当使用许多锁

4.3 时钟
时钟被用来轮询或进行超时操作的。一个驱动程序可以产生一个时钟并与一个函数关联上。当一个特定周期时钟期满时,调用相关函数。时钟可以是一次的或周期性的,一但设置了一个周期时钟,当每个周期结束时都会触发,直到它被完全清除掉为止。一次性时钟在触发后必须重新设置
时钟通过调用NdisMInitializeTimer来产生和初始化,并且通过调用NdisMsetTimer来设置,也可调用NdisMsetPeriodicTimer设置周期时钟。如果使用了一个非周期时钟,那么通过调用NdisMSetPeriodicTimer重新设置时钟。通过调用NdisMCancelTimer可以清除时钟

4.4 事件
事件在两个执行线程之间实现同步操作。一个事件通过一个驱动程序装入并且通过调用NdisInitializeEvent初始化。一个运行在IRQL PASSIVE_LEVEL下的线程调用NdisWaitEvent来将自身转入等侯状态。当一个驱动程序线程等待一个事件时,它指定了最大等待时间即等待事件的时间。当调用NdisSetEvent使时间得到信号量,或最大等待时间段结束时,它们两个无论是谁先发生时都将结束线程等待状态
典型的,事件是通过相互协调的线程调用NdisSetEvent来设置的。事件被创建时是没有信号量的,但为了指示等待线程,它必须要设置信号量,事件将一直处于保持有信号状态,直到NdiResetEvent调用后为止

5. 包结构
通过一个协议驱动程序可以分配NDIS包、填充数据,并且将它传递到下层的NDIS驱动程序,以便将数据发送到网络上。一些最底层的NIC驱动程序分配包用来保存接收到的数据,并将包传递到对应的高层驱 

6. 使用共享内存
用作总线管理DMA设备的微端口驱动程序必须为NIC和NIC驱动程序分配共享内存。当在一个驱动程序和它的NIC之间共享cache时,特别的预防是必须的。在某种结构下,必须采取特别步骤来保证内存一致,因为NIC可以直接访问共享的物理内存,而NIC驱动程序却要通过cache访问内存。这就引起驱动程序和NIC访问内存的不同,即使它们看起来在同一位置

7. 异步I/O和完成函数
因为在一些网络操作中有继承的因素,许多由NIC驱动程序提供的上层函数和协议驱动程序提供的下层函数被设计成支持异步操作,而不是用CPU消耗一定时间的循环来等待一个任务的完成或硬件事件的指示,网络驱动程序依赖处理许多异步操作的能力
通过使用完成函数来支持异步网络I/O。以下的例子将说明网络的send操作如何使用一个完成函数,同样的机制也存在一个协议或NIC驱动程序的其他操作中
当协议驱动程序调用NDIS发送一个包时,NDIS调用NIC驱动程序的MiniportSend函数发送请求,NIC驱动程序试图立即完成这个请求并且返回一个恰当的状态值。对于同步操作,可能返回NDIS_STATUS_SUCCESS作为发送成功的标志,NDIS_STATUS_RESOURCES和NDIS_STATUS_FAILURE表明有某些失败。
但是一个发送操作要花费一些时间来完成,此时NIC驱动程序(或NDIS)可将包排队并且等侯NIC指示发送操作的结果。NIC驱动程序的MiniportSend函数可以通过返回一个NDIS_STATUS_PENDING的状态值来异步处理这个操作,当NIC驱动程序完成了发送操作后,包调用完成函数NdisMSendComplete在调用中传递指向一个已被发送的包的描述符的指针。这个信息会传给协议驱动程序,指示完成了操作
许多需要一定时间来完成的驱动程序操作用完成数来完成支持异步的操作。这种函数有同一形式的名字NidisMXxxComplete。不仅可用于发送和接收函数,完成函数也可用于查询、配置、重新设置硬件、状态指示、指示收到数据和传送收到数据

0x7: NDIS协议驱动的实现

1. 初始化
协议驱动程序必须提供一个入口函数DrverEntry 函数,相当与C 应用程序的main 函数,驱动程序也是从这里开始被调用执行的。DrverEntry 函数主要功能是
    1) 对协议驱动中必须使用到的一系列protocol 函数进行注册,并对驱动数据结构及资源进行初始化操作
    2) 在 DriverEntry 必须调用NdisRegisterProtocol 函数注册驱动的Protocol 函数入口点
    3) 并且必须注册ProtocolBindAdapter 和ProtocolUnBindAdapter 函数,以便在ProtocolBindAdapter 函数中继续完成初始化工作
    4) DriverEntry 函数中还需要注册相关派遣例程等
Protocol 函数注册后,驱动运行时会自动调用相关Protocol 函数完成相应功能。

2. 动态绑定
协议驱动程序通过提供ProtocolBindAdapter 函数和ProtocolUnbindAdapter 函数就可以支持对低层网卡的动态绑定(本质是一个高层接口规范,是底层物理网卡必须遵循和实现的一个接口规范)。如果驱动程序向NDIS 注册了这些函数,那么将可以延迟打开和绑定低层网卡而不必在DriverEntry 函数中实现该功能,只要用ProtocolBindAdapter 函数就可代替执行该操作
如果协议驱动程序提供了这些函数,那么只要低层网卡可用,NDIS 将调用能够将自己绑定到该适配器的任何协议驱动程序的ProtocolBindAdapter 函数。并且只要低层网卡被关闭,那么NDIS 将可以调用互逆的ProtocolUnbindAdapter 函数 

3. 数据包的组织和管理
在 NDIS 中,数据的接收与发送都是以包为单位进行的。一个包由以下3 部分组成 
    1) 一个包描述符。其所含信息包括整个包所占用的物理页面的数量、包的长度、指向第一个和最后一个缓冲区描述符的指针以及包池的句柄等等 
    2) 一组缓冲区描述符。每个缓冲区描述符r 用来描述一片存储区域,其中包括起始虚拟地址、偏移量、该存储区域的大小以及指向下一个缓冲区描述符的指针等信息
    3) 由缓冲区描述符所描述的虚拟存储区域,该区域可能横跨几个页面。这些页面最终被射到物理内存中 

4. 接收数据
驱动初始化时会注册两个接收函数,ProtocolRecieve 和ProtocolRecievePacket 函数,当有数据包发送到网卡时,驱动会根据网卡类型调用所需函数,不支持多包接收的网卡或者接收单包且包描述符附有带外数据的情况下会调用ProtocolRecieve 函数,而多包接收的网卡会调用ProtocolRecievePacket 函数

驱动程序负责维护一个接收缓冲区,该缓冲区以队列的形式组织。当网卡通知NDIS已从网络上接收到数据包时,可以先将这些数据缓存起来。当上层应用程序需要读取数据时,该读操作的数据源不是直接从网络得到,而是经过有效管理的存放着数据的缓冲区

5. 发送数据
发送数据必须先组成需要发送的包格式,然后放到队列中,等待发送。在发送前,要创建发送数据包池和缓冲池(在绑定时候创建),在需要发送数据时为包从包池中创建包描述符和缓冲描述符
当数据包组好后,首先对数据的合法性检验。如果数据太短,小于以太网包头(14 个字节),则不合法;如果数据太长,比底层网卡所能支持的最大帧长度还长,则也不合法;然后判断是否已经绑定网卡,如果已绑定则调用系统函数NdisAllocatePacket 分配和初始化一个包描述符,调用NdisAllocateBuffer 分配和初始化缓冲描述符,并给新分配得到的缓冲描述符赋值,指向系统空间中的用户缓冲区,当资源分配完毕,会根据需要调用系统函数NdisSendPackets 或NdisSend 向下递交一个发送请求

对 NdisSendPackets 的调用,系统会自动调用ProtocolSendComplete来完成发送操作,获得发送的包的缓冲区信息,并释放包缓冲区描述符
收包由响应网卡产生的中断开始,发包由TDI传入协议驱动的IRP开始

0x8: NDIS包描述符结构

NDIS包描述符在协议驱动和小端口驱动中都有用到,在WDK中定义了NDIS_PACKET结构

typedef struct _NDIS_PACKET {
  NDIS_PACKET_PRIVATE Private;
  union {
    struct {
      UCHAR MiniportReserved[2*sizeof(PVOID)];
      UCHAR WrapperReserved[2*sizeof(PVOID)];
    };
    struct {
      UCHAR MiniportReservedEx[3*sizeof(PVOID)];
      UCHAR WrapperReservedEx[sizeof(PVOID)];
    };
    struct {
      UCHAR MacReserved[4*sizeof(PVOID)];
    };
  };
  ULONG_PTR           Reserved[2];
  UCHAR               ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;

需要明白的,NDIS的数据包是以一种指针队列的形式存在的,也就是说,网卡收到数据库产生中断,通过Buffer缓存数据包,NDIS和网卡通过Buffer进行异步协同

Relevant Link:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff557086(v=vs.85).aspx

 

3. NDIS微端口驱动编程实例

一个NDIS微端口驱动程序(也叫微端口NIC驱动程序)有两个基本功能

1. 管理一个网络接口卡(NIC),包括通过NIC发送和接收数据
2. 与高层驱动程序相接,例如中间层驱动程序和传输协议驱动程序 

发送和接收操作表明了NDIS与高层驱动程序和微端口NIC的相互作用

1. 当一个传输驱动程序发送一个包时,它调用一个NDIS库所提供的NdisXxx函数,NDIS于是通过调用由微端口提供的合适的MiniportXxx函数将包传递给微端口,然后微端口驱动程序通过调用恰当的NdisXxx函数将包传递给NIC来发送包
2. 当一个NIC接收到由NIC发给它的包时,它将产生一个由NDIS或NIC的微端口处理的中断。NDIS通过调用恰当的MiniportXxx函数指示NIC的微端口。微端口通过调用恰当的NdisXxx函数把数据从NIC传送到上层驱动程序,并且同时指示上层驱动程序接收包(NDIS在这里起到承上启下作用,它再去调用对应的ProtocolXxx函数)

0x1: NIC微端口驱动程序类型

NDIS既支持无连接环境下的微端口驱动程序,也支持面向连接的微端口驱动程序,NDIS支持以下几种类型的网络接口卡(NIC)驱动程序

1. 无连接的微端口
例如Ethernt,FDDI和Token Ring,控制NIC。可将无连接微端口进一步分为以下几种子类型 
    1) 串行化的驱动程序: 它依靠NDIS对MiniportXxx函数调用进行串行化,并管理它们的发送队列
    2) 非串行化驱动程序: 它自己对MiniportXxx函数操作进行串行化,并且在内部对进入的发送包进行排队。它的意义在于有很高的效率,例如驱动程序的临界区(在一个时间段内只有一个线程可访问的代码区)保持很小,但是,非串行化微端口必须满足额外的并且是极苛刻的设计要求,同时还要求有额外的调试和测试时间
2. 面向连接的微端
面向连接的网络介质,例如ATM和ISDN,控制NIC。面向连接的微端口经常对MiniportXxx函数操作进行串行化,并且在内部对进入的发送抱进行排队 

一个NDIS微端口驱动程序可以有一个非NDIS的底层,通过它的非NDIS底层,微端口使用某种类型的接口,例如通用串行总线构架(USB)或IEEEE1394(火线)来控制总线上的设备。微端口通过直接发送I/O请求包(IRPs)到总线或是发送到连接到总线的其他远程设备上来与设备通信。在它的上层,微端口提供了标准的NDIS微端口接口,它使得微端口可以与上层NDIS驱动程序通信
NDIS也支持广域网(WAN)范围的控制WAN NIC的微端口

0x2: 网络接口卡支持

Windows 2000支持以下几种类型的网络接口卡

1. Ethernet (802.3)
2. Token Ring (802.5)
3. FDDI
4. LocalTalk
5. Raw ARCNET (ARCNET 封装在 Ethernet)
6. ARCNET 878.2
7. WAN(点对点和WAN卡)
8. 面向连接的WAN
9. 无线
10. ATM
11. IrDA

当NDIS调用一个NIC驱动程序的MiniportInitialize函数时,它传递了一组NDIS支持的介质。如果NIC驱动程序仅支持一种介质类型,它选择它所支持的介质。或者,如果NIC驱动程序支持多于一种的介质类型,它选择它所喜欢的那一种并且将它的选择返回给NDIS
当一个高层NDIS协议驱动程序调用NdisOpenAdapter来绑定一个具体的NIC时,它提供了一系列它所能操作的介质类型,NDIS使用来自NIC的信息和来自协议驱动程序的信息来正确绑定它们。这个绑定提供了一条路径,通过这条路径,包可以在协议栈中往下传递并通过网络传递出去,通过这条路径,接收到的包可以传到高层驱动程序

0x3: 微端口驱动程序代码的重要特征

1. MiniportXxx函数
典型的微端口驱动程序仅使用很少的函数来通过NDIS与上层和硬件进行通信。NDIS为了完成自身的任务或协议驱动程序的服务,调用(MiniportXxx)函数列表与微端口通信;并不是所有的这些函数都是必须的,作为一个最基本的微端口驱动程序来说,它只需要满足最低程度的NDIS的接口规范即可正常运行

2. NDIS NIC微端口和上层驱动程序使用NDIS库(ndis.sys)通过调用NdisXxx函数在彼此间通信。许多微端口函数可以异步运行或同步运行
    1) 对于异步函数,当操作完成以后,必须调用NdisXxx...Complete。例如,如果一个协议驱动程序调用NdisReset来复位一个NIC,微端口的MiniportReset函数通过返回NDIS_STATUS_PENDING来挂起复位操作。最终,MiniportReset函数必须调用NdisMResetComplete来指示复位请求的最终状态  

3. 与NDIS库链接
NDIS库封装在ndis.sys中,这个内核模式库提供了一系列函数接口,它侧重于使功能最大化的宏。提供接口的库是一个.sys文件,它的函数与动态链接库相似。包括协议和NIC驱动程序,以及WAN  NIC驱动程序,它们都与NDIS库相链接

4. 微端口适配器环境
NDIS使用叫一个"逻辑适配器"的软件对象来代表系统中每一个NIC。这个对象由NDIS管理,并且对微端口和协议驱动程序是透明的。NDIS将这个结构的句柄传递给NIC驱动程序的MiniportInitialize函数。以后微端口驱动程序在调用NdisXxx函数时,如果这个函数与此句柄表示的NIC有关,它应在函数中传递此句柄 
当调用微端口NIC驱动程序来初始化一个它管理的NIC时,微端口驱动程序产生自己的内部数据结构来代表NIC。驱动程序使用这个结构,把它当作微端口适配器环境,来维护特定设备的状态信息,这些信息是管理NIC所必须的。当驱动程序的MiniportInitialize函数调用NdisMSetAttributes或NdisMSetAttributesEx时,它将这个结构的句柄传递给NDIS。当NDIS调用与NIC相关的微端口的MiniportXxx函数时,它将指向NIC的句柄传递给适当驱动程序。微端口适配器环境为微端口所拥用并且为微端口所管理,它对于NDIS和协议驱动程序是透明的

5. VC环境
在建立一个呼叫之前,面向连接的客户方请求面向连接的微端口建立一个虚拟连接(VC),通过它可以发送和/或接收包。同样,在向面向连接的客户方指示一个入站呼叫之前,呼叫管理器或集成的微端口呼叫管理器(MCM)请求微端口为入站呼叫建立一个VC。一个虚拟连接是两个面向连接实体间的一个逻辑连接。面向连接的发送和接收通常在一个特定的VC上进行。
面向连接微端口在微端口在为每个VC分配的环境区中保存状态信息。每一个VC的环境由微端口来管理,并且它对于NDIS和协议驱动程序是透明的。在它的MiniportCoCreateVC函数中,面向连接的微端口将VC环境区域的句柄传递给NDIS,NDIS将一个唯一指定的已存在的VC的NdisVcHandle回传给微端口、回传给适当的的面向连接的客户方、回传给呼叫管理器或集成微端口呼叫管理器(MCM)
在VC上发送和接收数据之前,VC必须被激话。呼叫管理器通过调用Ndis(M)CmDeactivateVc来初始化激活的VC并且向呼叫管理器传递呼叫参数,这包括用来激活VC的参数。作为响应,NDIS调用微端口的MiniportCoActivateVc函数来激活VC。
在一个呼叫结束或因其他原因不再需要VC时,呼叫管理器通过调用Ndis(M)CmDeactivateVc来去活VC,这也将引起NDIS调用微端口的MiniportCoDeactivateVc函数。对于面向连接的客户方或呼收管理器,可以通过调用NdisCoDeleteVc来指示对VC的删除,它将引起NDIS调用微端口的MiniportCoDeleteVc函数 

6. 网络OID
微端口维护有关它的性能和当前状况的信息,以及有关它所控制的每个NIC的信息。每一个信息类型都由一个对象标识(OID)确认,OID由系统定义 
NDIS和高层驱动程序可以使用OID来查询信息以及在某些情况下设置信息 
    1) 对于无连接介质的高层驱动程序,它调用NdisRequest来查询或设置一个无连接微端口中的信息。为了执行查询操作,NDIS调用微端口的MiniportQueryInformation函数。为了执行设置操作,NDIS调用微端口的MiniportSetInformation函数 
    2) 对于面向连接介质的高层驱动程序,它调用NdisCoRequest来查询或设置面向连接微端口中的信息。为了执行查询和设置操作,NDIS调用微端口的MiniportCoRequest函数 
NDIS将许多系统为微端口定义的OID映射为全局的唯一标识(GUIDs)。NDIS向内核模式的Windows管理检测器(WMI)来注册这些GUIDS。WMI支持用户模式下的基于Web的企业管理(WBEM)应用。当一个WMI客户方查询或设置这些GUIDS中的一个值时,NDIS将这些请求转换为一个恰当的OID查询操作或一个OID设置操作,并且把信息和状态返回给WMI。驱动程序编写者可将自定义的GUID映射为客户OID或微端口状态。一个微端口必须在初始化时向NDIS注册客户的GUID-to-OID或GUID-to-status映射 

0x4: NDIS微端口驱动程序入口函数

每个NIC微端口驱动程序必须提供一个DriverEntry的函数。DriverEntry由系统调用来装载驱动程序。DriverEntry产生一个微端口NIC驱动程序和NDIS库的连接,并且向NDIS注册微端口的版本和入口指针

..
NDIS_HANDLE        NdisWrapperHandle;
..
NTSTATUS
DriverEntry(
    //指向驱动程序对象的指针,它由I/O系统产生
    IN PDRIVER_OBJECT        DriverObject,
    /*
    指向驱动程序对象的指针,它由I/O系统产生
    注册表包含系统重起时永久存在的数据,它同每次系统重起时重新产生的设置信息一样。在驱动程序安装时,描述驱动程序和NIC的数据存储在注册表,注册表包含NIC驱动程序可读取的,用来初始化自身和NIC的适配器的特性
    */
    IN PUNICODE_STRING       RegistryPath
    )
{
    NDIS_MINIPORT_CHARACTERISTICS      MChars;
    ..
    /*
    1. 初始化包裹
    调用NdisMInitializeWrapper函数来使得微端口NIC驱动程序和NDIS相联系,NdisMInitializeWrapper分配一个结构来代表这个联系,来存储NDIS库所需的微端口相关的信息,并且返回NdisWrapperHandle
    它是代表这个微端口NIC驱动程序结构的句柄。当驱动程序注册它的入口指针时,它必须保留和传递这个句柄到NdisMRegisterMiniport。NDIS将使用NdisWrapperHandle来辩别微端口,微端口必须保留这个句柄,但它不应试图去访问或解释这个句柄
    */
    NdisMInitializeWrapper(&NdisWrapperHandle, DriverObject, RegistryPath, NULL);
    ..
    
    /*
    2. 注册微端口
    在DriverEntry函数调用NdisMInitializeWrapper产生与NDIS库的一个连接后,它必须初始化一个NDIS_MINIPORT_CHARACTERISTICS类型的结构。NDIS_MINIPORT_CHARACTERISTICS结构指定了与微端口兼容的NDIS版本以及要求的入口点和由微端口提供的可选上层函数(MiniportXxx)。然后DriverEntry通过一个指向NDIS_MINIPORT_CHARACTERISTICS结构的指针调用NdisMRegisterMiniport
    */
    /*
    2.1 指定NDIS版本号
    NDIS_MINIPORT_CHARACTERISTICS结构中的MajorNdisVersion和MinorNdisVersion成员指定了与微端口兼容的NDIS版本。有效的NDIS版本号是5.0,4.0,或3.0。最新版本是5.0。NDIS继续支持早期为4.0和3.0版编写的驱动程序。因此NDIS版本号,包括包含在NDIS_MINIPORT_CHARACTERISTICS中的,都必须在微端口源代码编释时指定
        */
    MChars.MajorNdisVersion = PASSTHRU_MAJOR_NDIS_VERSION;
        MChars.MinorNdisVersion = PASSTHRU_MINOR_NDIS_VERSION;
    /*
    2.2 注册MiniportXxx函数
    NDIS_MINIPORT_CHARACTERISTICS结构中每个地址成员必须被初始化——也就是说,它必须被设置成微端口提供的函数地址或NULL。微端口必须为所有MiniportXxx函数提供入口指针;否则NDIS将不允许装载驱动程序。MiniportXxx函数在ndis.h头文件中定义。
    因为微端口提供MiniportXxx函数的地址,而不是名字,驱动程序开发者可以自由的以他们喜好命名。一个合理的微端口开发策略是给这函数命名以使调试容易。例如,微端口NIC驱动程序mp.sys命名入口指针为MPISR,MpSend等 
    一些MiniportXxx函数是可选择的(之前说过,微端口驱动只需要满足NDIS最基本的可满足运行的接口规范即可)。例如,如果一个NIC不产生中断,微端口必须轮询它的NIC。这种微端口必须有一个MiniportTimer函数,但不需要MiniportISR函数、MiniportEnableInterrupt函数或MiniportDisableInterrupt函数 
        */
    MChars.InitializeHandler = MPInitialize;
        MChars.QueryInformationHandler = MPQueryInformation;
        MChars.SetInformationHandler = MPSetInformation;
        MChars.ResetHandler = NULL;
        MChars.TransferDataHandler = MPTransferData;
        MChars.HaltHandler = MPHalt;
#ifdef NDIS51_MINIPORT
        MChars.CancelSendPacketsHandler = MPCancelSendPackets;
        MChars.PnPEventNotifyHandler = MPDevicePnPEvent;
        MChars.AdapterShutdownHandler = MPAdapterShutdown;
#endif // NDIS51_MINIPORT 
        MChars.CheckForHangHandler = NULL;
        MChars.ReturnPacketHandler = MPReturnPacket; 
        MChars.SendHandler = NULL;    // MPSend;
        MChars.SendPacketsHandler = MPSendPackets;

    Status = NdisIMRegisterLayeredMiniport(NdisWrapperHandle,
                                                  &MChars,
                                                  sizeof(MChars),
                                                  &DriverHandle);
}

0x5: NDIS微端口初始化

当一个微端口DriverEntry函数返回时,NDIS立即为每个微端口管理的NIC,调用微端口的MiniportInitialize函数。如果一个新的NIC插入到系统,将调用微端口的MiniportInitialize函数来初始化新安装的设备。因此, MiniportInitialize函数和其他调用函数,以及所有运行在IRQL PASSIVE_LEVEL的函数都可被指定为可分页的。通过使用NDIS_PAGABLE_FUNCTTON宏来指定代码为可分页的。初始化代码不能用NDIS_INIT_FUNCTION宏来指定,这是由于标记这种方式的代码在初始化系统的启动结束之际,已不再映射了。仅有DriverEntry函数和从DrireEntry调用的函数可以传递到NDIS_INTI_FUNCTLON宏

NDIS_STATUS
MPInitialize(
    OUT PNDIS_STATUS             OpenErrorStatus,
    //微端口必须选择它所支持或兼容的介质。如果微端口没有在MediumArray中发现它所支持的介质,将返回一个失败状态NDIS_STATUS_UNSUPPORTED_MEDIA 
    OUT PUINT                    SelectedMediumIndex,
    IN  PNDIS_MEDIUM             MediumArray,
    IN  UINT                     MediumArraySize,
    //MiniportAdapterHandle,一个可被NDIS引用的微端口句柄。微端口必须保留这个句柄,以使它在以后的调用中可传递给NDIS。例如,在NdisMRegisterAdapterShutdownHandler和NdisMInitializeTimer调用中
    IN  NDIS_HANDLE              MiniportAdapterHandle,
    //MiniportAdapterHandle,一个可被NDIS引用的参照微端口句柄。微端口必须保留这个句柄,以使它在以后的调用中可传递给NDIS。——例如,在NdisMRegisterAdapterShutdownHandler和NdisMInitializeTimer调用中
    IN  NDIS_HANDLE              WrapperConfigurationContext
    )
{
    ..
    /*
    1. retrieving our adapter context and storing the Miniport handle in it
    */
    pAdapt = NdisIMGetDeviceContext(MiniportAdapterHandle);
        pAdapt->MiniportIsHalted = FALSE; 

        Medium = pAdapt->Medium; 
        if (Medium == NdisMediumWan)
        {
            Medium = NdisMedium802_3;
        } 
        for (i = 0; i < MediumArraySize; i++)
        {
            if (MediumArray[i] == Medium)
            {
                *SelectedMediumIndex = i;
                break;
            }
        } 
        if (i == MediumArraySize)
        {
            Status = NDIS_STATUS_UNSUPPORTED_MEDIA;
            break;
        }
    ..
    /*
    2. 注册NIC
    在微端口读取它所需要的描述有关NIC特征的信息,并且按要求方式将此信息存储在它的微端口适配器环境中之后,微端口调用NdisMSetAttributes或NdisMSetAttributeEx。在这个调用中,微端口作如下工作
    2.1) 传递由NDIS提供的微端口适配器句柄到微端口的MiniportInitialize函数
    2.2) 传递微端口适配器环境句柄,当NDIS调用任何MiniportXxx函数时,NDIS将这个适配器指定环境区域的句柄传递给微端口
    2.3) 指定它的NIC是否是一个总线管理器DMA设备
    2.4) 指定调用者的NIC的I/O总线接口类型
    通过NdisMSetAttributesEx,微端口可以指定有关超时和Token Ring错误的NDIS缺省行为的变化
    中间层驱动程序必须设置NDIS_ATTRIBUTE_INTERMEDLATE_DRIVER标志来调用NdisMSetAttributerEx
    中间层驱动程序也必须设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志,来阻止微端口在系统转向低耗能(睡眠)状态时,停止驱动程序 
    一个非串行微端口必须设置NDIS_ATTRIBUTE_DESERIALIIE标志来调用NdisMSetAttrributerEx。一个面向连接的微端口,总是非串行的,所以并不需要这么做
    */

    //3. Create an ioctl interface 
        (VOID)PtRegisterDevice();
    ..
}

0X6: MiniportXxx: 微端口接口函数逻辑实现

在初始化并注册了微端口的描述句柄后(如果是在NDIS中间层协议中就仅仅是一个虚拟的微端口是配置,但是从内核逻辑层次上来说,它就是一个标准的微端口),需要对微端口描述句柄中赋值的函数句柄进行逻辑功能的实现

VOID
MPSendPackets(
    IN NDIS_HANDLE             MiniportAdapterContext,
    IN PPNDIS_PACKET           PacketArray,
    IN UINT                    NumberOfPackets
    ) 
{
    PADAPT              pAdapt = (PADAPT)MiniportAdapterContext;
    FILTER_STATUS        fStatus;
    NDIS_STATUS         Status;
    UINT                i;
    PVOID               MediaSpecificInfo = NULL;
    UINT                MediaSpecificInfoSize = 0;
    

    for (i = 0; i < NumberOfPackets; i++)
    {
        PNDIS_PACKET    Packet, MyPacket;

        Packet = PacketArray[i];


        fStatus = AnalysisPacket(Packet, FALSE);
        if(fStatus == STATUS_DROP){
            // 在这个函数中,任何一个被放弃的包,都必须调用NdisMSendComplete。
            NdisMSendComplete(ADAPT_MINIPORT_HANDLE(pAdapt),
                Packet,
                NDIS_STATUS_FAILURE);
            continue; // 丢弃之
        }else if(fStatus == STATUS_REDIRECT){
            // 转发......
            // TODO.
        }
        ..
..

NDIS_STATUS
MPTransferData(
    OUT PNDIS_PACKET            Packet,
    OUT PUINT                   BytesTransferred,
    IN NDIS_HANDLE              MiniportAdapterContext,
    IN NDIS_HANDLE              MiniportReceiveContext,
    IN UINT                     ByteOffset,
    IN UINT                     BytesToTransfer
    ) 
{
    PADAPT        pAdapt = (PADAPT)MiniportAdapterContext;
    NDIS_STATUS   Status; 

    if (IsIMDeviceStateOn(pAdapt) == FALSE)
    {
        return NDIS_STATUS_FAILURE;
    }

    NdisTransferData(&Status,
                     pAdapt->BindingHandle,
                     MiniportReceiveContext,
                     ByteOffset,
                     BytesToTransfer,
                     Packet,
                     BytesTransferred);

    if(Status == NDIS_STATUS_SUCCESS){

        // 已经完全传递好了,是一个完整的NDIS Packet
        FILTER_STATUS fStatus = AnalysisPacket(Packet, TRUE);

        if(fStatus == STATUS_DROP){
            Status = NDIS_STATUS_FAILURE; // 丢弃之
        }else if(fStatus == STATUS_REDIRECT){
            // 转发......
            // TODO.
        }
    }

    return(Status);
}
..

NDIS_STATUS
MPSend(
    IN NDIS_HANDLE             MiniportAdapterContext,
    IN PNDIS_PACKET            Packet,
    IN UINT                    Flags
    ) 
{
    PADAPT              pAdapt = (PADAPT)MiniportAdapterContext;
    NDIS_STATUS         Status;
    FILTER_STATUS        fStatus;
    PNDIS_PACKET        MyPacket;
    PVOID               MediaSpecificInfo = NULL;
    ULONG               MediaSpecificInfoSize = 0; 

    if (pAdapt->MPDeviceState > NdisDeviceStateD0)
    {
         return NDIS_STATUS_FAILURE;
    }

    fStatus = AnalysisPacket(Packet, FALSE);
    if(fStatus == STATUS_DROP){
        return NDIS_STATUS_FAILURE; // 丢弃之
    }else if(fStatus == STATUS_REDIRECT){
        // 转发......
        // 修改包中的目标地址,和正常发送一样将包向下发送
        // TODO.
    }
    ..
..

对于NDIS过滤驱动来说,我们可以在这里拦截到所有的本机将要外发的数据包

Relevant Link:

http://files.cnblogs.com/files/LittleHann/netdriver.pdf

 

4. NDIS中间层驱动编程实例

虽然向上层提供了MiniportXxx函数的一个子集,但是事实上中间层驱动程序并不管理物理的NIC。它只是向上层协议提供了一个或多个可以绑定的虚拟适配器。对于一个协议驱动程序,一个由中间层驱动程序提供的适配器就如同一个物理的NIC。当协议驱动程序发送包或向一个实际的适配器时发出请求时,中间层驱动程序将传递这些包和请求到下层的微端口。当下层微端口向上发出接收包的指示、响应协议请求的信息或指示状态信息时,中间层驱动程序将这些包、响应、和状态指示传递给绑定在虚拟适配器上的协议驱动程序
中间层驱动程序有如下几种典型使用方法

1. 在不同的网络介质间进行转换
例如,处于Ethernet和Token Ring传输层及一个ATM微端口之间的中间层驱动程序的功能是将Ethenet和Token Ring的包转换为ATM包,反之亦然 

2. 过滤包
一个包调度器是中间层驱动程序用作过滤驱动程序的很好例子。包调度器读出每个传输层传下来的发送数据包和每一个由微端口指示的接收包的优先权。然后包调度器以它的优先权调度抱的接收和发送顺序 

3. 在多个NIC间平衡包的负载
负载平衡驱动程序向上层传输协议提供了一个虚拟的适配器,但实际上它却将包分配了给多个NIC 

为了使加载程序能够准确地识别,必须将中间层驱动程序的初始入口点明确地指定为DriverEntry的形式。所有其他的驱动程序导出函数,像这里所描述的MiniportXxx和ProtocolXxx函数,由于其地址传给了NDIS,因此在设计时可由开发者任意指定名称,在中间层驱动程序中,DriverEntry至少应该完成以下工作

1. 调用NdisMInitializeWrapper并保存在NdisWrapperHandle中返回的句柄 
2. 传递上一步保存的句柄,调用NdisIMRegisterLayeredMiniport注册驱动程序的MiniportXxx函数 
3. 如果驱动程序随后要绑定到低层NDIS驱动程序上,则调用NdisRegisterProtocol注册驱动程序的ProtocolXxx函数 
4. 如果驱动程序导出了MiniportXxx和ProtocolXxx函数,那么调用NdisIMAssociateMiniport向NDIS通告有关驱动程序的微端口低边界和协议高边界信息 

DriverEntry能够为中间层驱动程序分配的所有共享资源初始化自旋锁,例如驱动程序用于跟踪运行中的连接和发送任务的构件和内存区
当DriverEntry不能为驱动程序分配用于网络I/O操作的所有资源时,它就应该释放先前已经分配的任何资源并返回一个适当的错误状态。例如,如果DriverEntry已经调用了NdisMInitializeWrapper函数,那么当后续操作出错时必须调用NdisTerminateWrapper复位系统状态。
中间层驱动程序的DriverEntry函数能够执行一些全局初始化操作。然而,如果驱动程序提供了实现对低层设备的打开和绑定功能的ProtocolBindAdapter函数,那么驱动程序就能够让ProtocolBindAdapter来分配绑定相关的系统资源,ProtocolBindAdapter根据要求为DeviceName设备进行资源分配和绑定操作。DriverEntry必须初始化该包裹程序并注册微端口驱动程序。如果中间层驱动程序导出了一组ProtocolXxx函数的话,也要注册协议驱动程序
如果中间层驱动程序仅向NDIS导出了一组MiniportXxx函数,只要向NDIS库注册这些函数即可

0x1: 中间层过滤驱动逻辑流程

1. 中间层驱动的初始化  [passthru]
    1) 初始化小端口部分: NdisMInitializeWrapper -> NDIS_MINIPORT_CHARACTERISTICS –> NdisIMRegisterLayeredMiniport
    2) 初始化协议驱动部分并关联至小端口: NDIS_PROTOCOL_CHARACTERISTICS -> NdisRegisterProtocol -> NdisIMAssociateMiniport
    3) passthru初始化总体结构
2. 封包结构分析与过滤  [TCP端口过滤]
    要对TCP端口进行过滤,很明显必须得先对TCP协议封包的结构进行分析。不同协议的封包格式都不尽相同,对封包格式有兴趣的童鞋可以使用wireshark多抓几个进行分析。TCP协议的封包总共有三个部分:第一是以太网部分,第二是IP部分(前两部分是所有协议的封包都有的),第三就是TCP部分了。
3. 获取封包
中间层驱动初始化完毕后,只需要在初始化时设置的相应回调函数进行处理即可。回调函数设置好了,封包的发送与接收都会调用我们的回调函数。过滤函数一般都在相应的收包发包函数内部的前端,如此一来可以提升处理效率。当然也省了不少麻烦(比如人家内存都分配好了,结果你要丢包)。不过也不死板,可以根据实际情况进行编写。对于发送出去的数据包处理,只要在PassThru中的MiniportSend中加入必要的操作代码,而对于接收的数据包时,则需要在ProtocolReceive和ProtocolReceviePackets中加入必要的操作代码
4. 过滤规则的编写
NDIS封包的过滤有很多种处理方式:延迟或重新排序、加密或解密、压缩或解压、包路由(NAT网络地址转换、LBFO负载平衡和失效替换)。如果修改了封包的内容的话,例如端口转发,需要重新修正校验和调整缓冲区和长度等相关信息。修改时需要注意字节顺序的问题 
5. 驱动程序加载方式:控制面板 -> 网络连接 -> 本地连接 -> 属性 -> 安装 -> 服务 -> 添加 -> 从磁盘安装 -> 选择我们inf文件,忽略一切警告一路确定即可 

NDIS中间层驱动源代码

http://files.cnblogs.com/files/LittleHann/passthru.zip

0x2: 中间层驱动发送数据包

中间层驱动之所以能被作为过滤驱动使用,是因为它处于上下交通的要冲之地,上下往来的数据包它都能够截获并加以处理
当上层驱动将数据包发送到中间层驱动时,中间层驱动能够在它注册的MpSend函数中接收到这些包描述符,这时候,中间层驱动要么直接数据包描述符传递给底层驱动;要么将数据重新打包生成新的描述符后,再发送出去;或者直接拒绝这个包并返回一个错误值(防火墙的包过滤功能,就是在这里体现的)
中间层驱动要发送网络数据包,最终都必须调用NdisSend/NdisSendPackets,要实现利用中间层驱动进行包发送,我们有如下几种方法

1. "包描述符重利用"技术
把收到的包描述符不动地传递下去(重放)

2. "包重申请"技术
根据收到的包描述符,重建一个新的包描述符,将新建的描述符发送下去

0x3: 中间层驱动数据包过滤分析逻辑

FILTER_STATUS AnalysisPacket(PNDIS_PACKET Packet,  BOOLEAN bRecOrSend)
{
    FILTER_STATUS status = STATUS_PASS; // 默认全部通过
    PNDIS_BUFFER NdisBuffer ;
    UINT TotalPacketLength = 0;
    UINT copysize = 0;
    UINT DataOffset = 0 ;
    UINT PhysicalBufferCount;
    UINT BufferCount   ;
    PUCHAR pPacketContent = NULL;
    char* tcsPrintBuf = NULL;
    PUCHAR tembuffer = NULL ; 
    UINT j;

    PTCPv4_HEADER pTCPHeader;
    USHORT iphdrlen;

    __try{

        status = NdisAllocateMemoryWithTag( &pPacketContent, 2048, TAG); 
        if( status != NDIS_STATUS_SUCCESS ){
            status = NDIS_STATUS_FAILURE ;
            __leave;
        }

        NdisZeroMemory( pPacketContent, 2048 ) ;

        // 找到第一个Ndis_Buffer。然后通过通过NdisGetNextBuffer来获得后续的NDIS_BUFFER。
        // 如果只是找第一个节点,更快且方便的方法是调用NdisGetFirstBufferFromPacket。
        NdisQueryPacket(Packet,  // NDIS_PACKET        
            &PhysicalBufferCount,// 内存中的物理块数
            &BufferCount,         // 多少个NDIS_BUFFER包
            &NdisBuffer,         // 将返回第一个包
            &TotalPacketLength     // 总共的包数据长度
            );

        while(TRUE){

            // 取得Ndis_Buffer中存储缓冲区的虚拟地址。
            // 这个函数的另一个版本是NdisQueryBuffer。
            // 后者在系统资源低或者甚至耗尽的时候,会产生Bug Check,导致蓝屏。
            NdisQueryBufferSafe(NdisBuffer,
                &tembuffer,// 缓冲区地址
                &copysize, // 缓冲区大小
                NormalPagePriority
                );

            // 如果tembuffer为NULL,说明当前系统资源匮乏。
            if(tembuffer != NULL){
                NdisMoveMemory( pPacketContent + DataOffset , tembuffer, copysize) ;            
                DataOffset += copysize;
            }

            // 获得下一个NDIS_BUFFER。
            // 如果得到的是一个NULL指针,说明已经到了链式缓冲区的末尾,我们的循环应该结束了。
            NdisGetNextBuffer(NdisBuffer , &NdisBuffer ) ;

            if( NdisBuffer == NULL )
                break ;
        }

        // 取得数据包内容后,下面将对其内容进行过滤。
        // 我们在这个函数中的实现,仅仅简单地打印一些可读的Log信息。
        if(pPacketContent[12] == 8 &&  pPacketContent[13] == 0 )  //is ip packet
        {    
            PIP_HEADER pIPHeader = (PIP_HEADER)(pPacketContent + IP_OFFSET);

            DBGPRINT(("╔════════════IP协议结构打印══════════\n"));
            DBGPRINT(("║    4位版本号:        %2x\n",  pIPHeader->Protocol));
            DBGPRINT(("║    8位服务类型(TOS): %2x\n",  pIPHeader->TOS));
            DBGPRINT(("║    16位总长度(字节): %2x\n",  pIPHeader->TotLen));
            DBGPRINT(("║    16位标识:         %2x\n",  pIPHeader->ID));
            DBGPRINT(("║    13位片偏移:       %2x\n",  pIPHeader->FlagOff));
            DBGPRINT(("║    8位生存时间(TTL): %2x\n",  pIPHeader->TTL));
            DBGPRINT(("║    8位协议:          %2x\n",  pIPHeader->Protocol));
            DBGPRINT(("║    16位首部校验和:   %2x\n",  pIPHeader->Checksum));
            DBGPRINT(("╚═════════════════════════════════\n"));

            switch(pIPHeader->Protocol)
            {
            case PROT_ICMP:
                if(bRecOrSend){
                    DBGPRINT(("Receive ICMP packet  [%d]\n",++ICMPRecv));
                }
                else{
                    DBGPRINT(("Send ICMP packet   [%d]\n",++ICMPSend));
                }

                //
                // 取得ICMP头,做出你的过滤判断。
                // 
                if(ICMP_PASS)
                {
                    return STATUS_DROP;
                }

                break;
            case PROT_UDP:
                if(bRecOrSend){
                    DBGPRINT(("Receive UDP packet  [%d]\n",++UDPRecv));
                }
                else{
                    DBGPRINT(("Send UDP packet   [%d]\n",++UDPSend));
                }

                //
                // 取得UDP头,做出你的过滤判断。
                //

                break;
            case PROT_TCP:
                if(bRecOrSend){
                    DBGPRINT(("Receive TCP packet  [%d]\n",++TCPRecv));
                }
                else{
                    DBGPRINT(("Send TCP packet   [%d]\n",++TCPSend));
                }

                //
                // 取得TCP头,做出你的过滤判断。
                //
                iphdrlen = (pIPHeader->VIHL & 0x0f) * sizeof(ULONG);
                
                pTCPHeader = (PTCPv4_HEADER)(pPacketContent+14 + iphdrlen);
                //tcphdrlen = ((pTCPHeader->dataoffset & 0xf0) >> 4) * sizeof(ULONG);
                //tcphdrlen = htons(pIPHeader->ipLength) - iphdrlen;
                
                DBGPRINT(("╔════════════TCP协议结构打印══════════\n"));
                DBGPRINT(("║    TCP 源端口:  %2x\n",pTCPHeader->SourcePort));
                DBGPRINT(("║    TCP 目的端口:%2x\n",pTCPHeader->DestinationPort));
                DBGPRINT(("║    32位序列号:  %2x\n",pTCPHeader->SequenceNumber));
                DBGPRINT(("║    32位确认号:  %2x\n",pTCPHeader->AckNumber));
                DBGPRINT(("║    4位首部长度: %2x\n",pTCPHeader->DataOffset));
                DBGPRINT(("║    保留(16位):  %2x\n",pTCPHeader->Flags));
                DBGPRINT(("║    16位窗口大小:%2x\n",pTCPHeader->Window));
                DBGPRINT(("║    校验和:      %2x\n",pTCPHeader->Checksum));
                DBGPRINT(("║    16位紧急指针:%2x\n",pTCPHeader->Urgent));
                DBGPRINT(("╚═════════════════════════════════\n"));

/*                if(pTCPHeader->SourcePort == TcpPort)
                {
                    return STATUS_DROP;
                }
                
                if(pTCPHeader->DestinationPort == TcpPort)
                {
                    return STATUS_DROP;
                }
    */            
                break;
            }
        }else if(pPacketContent[12] == 8 &&  pPacketContent[13] == 6 ){
            if(bRecOrSend){
                DBGPRINT(("Receive ARP packet  [%d]\n",++ARPRecv));

                 //分析接收的ARP封包
                 //    if(ARP_PROTECT){}
            }
            else{
                DBGPRINT(("Send ARP packet   [%d]\n",++ARPSend));

                //分析发送的ARP封包
                // if(ARP_PROTECT){}
            }
        }else{
            if(bRecOrSend)
                DbgPrint("Receive unknown packet\n");
            else
                DbgPrint("Send unknown packet\n");
        }

        // 简单打印出包数据内容
        status = NdisAllocateMemoryWithTag( &tcsPrintBuf, 2048*3, TAG);  //分配内存块
        if( status != NDIS_STATUS_SUCCESS ){
            status = NDIS_STATUS_FAILURE ;
            __leave;
        }
        for(j=0;j<=DataOffset;j++)
            RtlStringCbPrintfA(tcsPrintBuf+j*3, 2048*3-j*3, "%2x ",pPacketContent[j]);

        DbgPrint(tcsPrintBuf);
        DbgPrint("\n");

    }__finally{
        if(pPacketContent)NdisFreeMemory(pPacketContent, 0, 0);
        if(tcsPrintBuf)NdisFreeMemory(tcsPrintBuf, 0, 0);
    }

    return STATUS_PASS;
}

0x4: 用户态和中间层驱动通信

一种常见的需求是,希望从用户层或其他的内核模块中,能够直接和我们编写的NDIS中间层驱动进行通信,我们可以安装自己的分发函数,并且在私有的分发函数中继续调用旧的分发函数来继续兼容原始的内核逻辑,通过Hook设备创建函数,在内核态中创建我们私有的命名设备对象,从而在用户态使用write、read等简单接口进行ring3-ring0通信

// 这里实现Hook
// 
systemAddDevice = DriverObject->DriverExtension->AddDevice; 
DriverObject->DriverExtension->AddDevice = myAddDevice;

// Hook分发函数
//
systemCreate = DriverObject->MajorFunction[IRP_MJ_CREATE];
DriverObject->MajorFunction[IRP_MJ_CREATE] = myCreate;

systemWrite = DriverObject->MajorFunction[IRP_MJ_WRITE];
DriverObject->MajorFunction[IRP_MJ_WRITE] = myWrite;

systemRead = DriverObject->MajorFunction[IRP_MJ_READ];
DriverObject->MajorFunction[IRP_MJ_READ] = myRead;

systemDeviceControl = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL];
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = myDeviceControl;

Relevant Link:

寒江独钓-Windows内核安全编程(完整版).pdf

 

5. NDIS协议层驱动编程实例

协议驱动程序和NDIS进行通讯来发送和接收网络数据包,并绑定和使用低层微端口 NIC驱动程序或中间层NDIS驱动程序

1. NDIS协议驱动程序可能在上边界支持TDI,或者可以通过驱动程序的传送栈,其中包括栈顶支持TDI的栈,向高层核心模式驱动程序导出私有接口。例如,NDIS协议驱动程序可能是多模块传输实现的标准协议的最低层模块,如最高层模块支持TDI的TCP/IP协议
2. 低层NDIS驱动程序通信来发送和接收数据包的协议驱动程序总是使用NDIS提供的函数来进行通信。例如,下边界面向无连接的协议驱动程序(和面向无连接介质的低层驱动程序进行通信,如以太网、令牌环网)必须调用NdisSend或者NdisSendPackets向低层NDIS驱动程序发送数据包,也必须调用NdisRequest来产生或传送查询,以及用网络相关的低层面向无连接驱动程序支持的OID_XXX的设置信息请求
3. NDIS也提供一组隐藏低层操作系统细节的NdisXxx函数。例如,协议驱动程序调用NdisInitializeEvent为同步目的创建事件,调用NdisInitializeListHead创建链表。使用那些函数的NDIS版本的协议驱动程序在支持WIN32接口的微软操作系统中可移植性更好。协议驱动程序也可调用OS专用的核心模式支持的例程,像用KeInitializeEvent创建事件、用KeWaitForSingleObject同步两个执行线程 

0x1: 协议DriverEntry及其初始化

对于驱动程序初始化要求的入口点,为了使引导程序能够识别,必须被明确地命名为DriverEntry形式。所有其他的被描述为ProtocolXxx的导出函数,由于其地址被传给了NDIS,则可由开发者指定任何确定的名字

NTSTATUS
DriverEntry(
    IN PDRIVER_OBJECT        DriverObject,
    IN PUNICODE_STRING       RegistryPath
    )
{    
    //零初始化一个NDIS_PROTOCOL_CHARACTERISTICS类型的结构
    NDIS_PROTOCOL_CHARACTERISTICS      PChars;
    ..
    //填充协议描述句柄的成员字段
    NdisZeroMemory(&PChars, sizeof(NDIS_PROTOCOL_CHARACTERISTICS));
        PChars.MajorNdisVersion = PASSTHRU_PROT_MAJOR_NDIS_VERSION;
        PChars.MinorNdisVersion = PASSTHRU_PROT_MINOR_NDIS_VERSION;
 
        PChars.Name = Name;
    //OpenAdapterCompleteHandler: 这是一个必须提供的函数。如果协议驱动程序对NdisOpenAdapter的调用返回NDIS_STATUS_PENDING,则接着调用ProtocolOpenAdapterComplete来完成绑定操作
        PChars.OpenAdapterCompleteHandler = PtOpenAdapterComplete;
    //CloseAdapterCompleteHandler: 这是一个必须提供的函数。如果协议驱动程序对NdisCloseAdapter的调用返回NDIS_STATUS_PENDING,则接着调用ProtocolCloseAdapterComplete来完成解除绑定操作
        PChars.CloseAdapterCompleteHandler = PtCloseAdapterComplete;
        PChars.SendCompleteHandler = PtSendComplete;
        PChars.TransferDataCompleteHandler = PtTransferDataComplete;
    
        PChars.ResetCompleteHandler = PtResetComplete;
        PChars.RequestCompleteHandler = PtRequestComplete;
    //ReceiveHandler: 这是一个必须提供的函数。ProtocolReceive函数以前视缓冲区的指针为参数被调用执行。如果该缓冲区包含的不是完整的接收到的网络数据包,ProtocolReceive以协议分配的数据包描述符作为参数,调用NdisTransferData指定协议分配缓冲区接收数据包的其余部分
        PChars.ReceiveHandler = PtReceive;
    //ReceiveCompleteHandler: 这是一个必须提供的函数。ProtocolReceiveComplete用来指出:以前指示给ProtocolReceive 的接收数据包现在可以延期处理
        PChars.ReceiveCompleteHandler = PtReceiveComplete;
        PChars.StatusHandler = PtStatus;
        PChars.StatusCompleteHandler = PtStatusComplete;
    //BindAdapterHandler: 这是一个必须提供的函数。NDIS调用该函数请求协议驱动程序绑定到低层网卡或虚拟网卡上,网卡名作为该处理程序的参数传递
        PChars.BindAdapterHandler = PtBindAdapter;
    //UnbindAdapterHandler: 这是一个必须提供的函数。NDIS调用ProtocolUnbindAdapter释放对低层网卡或虚拟网卡的绑定,网卡名作为参数传递。当绑定成功解除时,ProtocolUnbindAdapter函数调用NdisCloseAdapter并释放资源
        PChars.UnbindAdapterHandler = PtUnbindAdapter;
        PChars.UnloadHandler = PtUnloadProtocol;

        PChars.ReceivePacketHandler = PtReceivePacket;
        PChars.PnPEventHandler= PtPNPHandler;

    /*
    1. 注册NDIS协议驱动程序
    该调用的返回句柄NdisProtocolHandler对协议驱动程序是透明的,协议驱动程序必须保存该句柄并在将来对NDIS的调用中作为输入参数传递,例如,打开低层适配器
    */
    NdisRegisterProtocol(&Status,
                             &ProtHandle,
                             &PChars,
                             sizeof(NDIS_PROTOCOL_CHARACTERISTICS));
}
..

0x2: ProtocolXxx协议驱动接口逻辑功能实现

对于NDIS过滤驱动来说,最重要的要实现的就是数据包接收函数

INT
PtReceivePacket(
    IN NDIS_HANDLE            ProtocolBindingContext,
    IN PNDIS_PACKET           Packet
    ) 
{
    PADAPT              pAdapt =(PADAPT)ProtocolBindingContext;
    NDIS_STATUS         Status;
    PNDIS_PACKET        MyPacket;
    BOOLEAN             Remaining;
    FILTER_STATUS        fStatus; 

    if ((!pAdapt->MiniportHandle) || (pAdapt->MPDeviceState > NdisDeviceStateD0))
    {
          return 0;
    }

    fStatus = AnalysisPacket(Packet, TRUE);
    if(fStatus == STATUS_DROP){
        return 0;// 丢弃之
    }else if(fStatus == STATUS_REDIRECT){
        // 转发......
        // TODO.
    }
    ..
..

VOID
PtTransferDataComplete(
    IN  NDIS_HANDLE         ProtocolBindingContext,
    IN  PNDIS_PACKET        Packet,
    IN  NDIS_STATUS         Status,
    IN  UINT                BytesTransferred
    ) 
{
    PADAPT      pAdapt =(PADAPT)ProtocolBindingContext;

    // 到达这里,说明MPTransferData返回了Pending
    FILTER_STATUS fStatus = AnalysisPacket(Packet, TRUE);
    if(fStatus == STATUS_DROP){
        Status = NDIS_STATUS_FAILURE;// 丢弃之
    }else if(fStatus == STATUS_REDIRECT){
        // 转发......
        // TODO.
    }

    if(pAdapt->MiniportHandle)
    {
        NdisMTransferDataComplete(pAdapt->MiniportHandle,
                                  Packet,
                                  Status,
                                  BytesTransferred);
    }
}

..

NDIS_STATUS
PtReceive(
    IN  NDIS_HANDLE         ProtocolBindingContext,
    IN  NDIS_HANDLE         MacReceiveContext,
    IN  PVOID               HeaderBuffer,
    IN  UINT                HeaderBufferSize,
    IN  PVOID               LookAheadBuffer,
    IN  UINT                LookAheadBufferSize,
    IN  UINT                PacketSize
    ) 
{
    PADAPT            pAdapt = (PADAPT)ProtocolBindingContext;
    PNDIS_PACKET      MyPacket, Packet = NULL;
    NDIS_STATUS       Status = NDIS_STATUS_SUCCESS;
    ULONG             Proc = KeGetCurrentProcessorNumber();      
    
    if ((!pAdapt->MiniportHandle) || (pAdapt->MPDeviceState > NdisDeviceStateD0))
    {
        Status = NDIS_STATUS_FAILURE;
    }
    else do
    {
        //
        // Get at the packet, if any, indicated up by the miniport below.
        //
        Packet = NdisGetReceivedPacket(pAdapt->BindingHandle, MacReceiveContext);
        if (Packet != NULL)
        {
            FILTER_STATUS fStatus = AnalysisPacket(Packet, TRUE);
            if(fStatus == STATUS_DROP){
                Status = NDIS_STATUS_FAILURE;// 丢弃之
            }else if(fStatus == STATUS_REDIRECT){
                // 转发......
                // TODO.
            }
        ..
..

我们可以在协议驱动的数据接收函数中实现Hook,插入过滤检测逻辑

Relevant Link:

http://www.ndis.com/
http://baike.baidu.com/view/1033533.htm
http://www.cppblog.com/aurain/archive/2009/02/22/74621.html
http://bbs.pediy.com/showthread.php?t=65053
NDIS协议驱动程序设计.pdf
http://www.cnblogs.com/RodYang/p/3221173.html

 

6. TDI驱动

0x1: TDI概要

TDI(Transport Driver Interface 传输层接口)仅在windows 2000~windows vista被支持,取代TDI的新技术被称为WFP(Windows Filtering Platform windows过滤平台)
我们在用户态使用socket进行socket创建、发送、接收数据,通过TDI接口连接NDIS协议驱动,TDI实际上是一套接口的集合,这套接口连接着socket和NDIS协议驱动
TDI协议驱动生成了一个有名设备,这个设备能接收一组请求

1. 生成请求(IRP_MJ_CREATE): 用于生成socket
2. 控制请求(IRP_MJ_DEVICE_CONTROL、IRP_MJ_INTERNAL_DEVICE_CONTROL): 用来实现所有复杂的功能,例如bind、connect、listen、accept、send、recv

我们知道,windows内核和核心概念就是设备栈,既然NDIS协议驱动也生成了设备,我们就可以生成新的过滤设备来绑定这些设备,这样,从应用层发来的请求首先会被过滤设备截获,很多防火墙显示某个应用程序要建立某个连接、打开了某个端口,就是用这种技术实现的
TDI过滤相比于NDIS中间层过滤的一大优势就是TDI离应用层比较近,容易得到应用层的相关信息,例如一个连接的建立时可以获得相应进程号(这也是我们在不同层次进行Hook的时候需要进行技术架构权衡),但是同时,如果黑客想写一个木马来绕过TDI接口,可以不调用一般网络API来避免调用TDI接口,NDIS用中间层进行过滤相对底层,从应用层很难绕过

 

7. TDI的过滤框架

0x1: 绑定TDI的设备

提供TDI接口的windows协议驱动将在windows内核中生成所谓的TDI设备,设备是由路径和名字的,例如

1. \Device\Tcp: TCP协议
2. \Device\Udp: UDP协议
3. \Device\RawIp: 原始IP包

我们如果要过滤TDI接口,那么首先就要生成自己的设备,用来绑定当前主机存在的协议驱动

0x2: 唯一的分发函数

如果一个驱动生成了设备,那么设置处理windows发送给这些设备请求的分发处理函数

0x3: 过滤框架的实现

下面我们继续讨论构建一个TDI过滤框架,这个内核模块可以过滤3种协议设备的TDI请求,框架绑定了3个设备(和NDIS一样,如果想要接收所有本机的收包,就要逐个去绑定底层的所有的微端口),这3个设备是; \Device\Tcp: TCP协议、\Device\Udp: UDP协议、\Device\RawIp: 原始IP包

#include <ntddk.h>
#include <tdikrnl.h>
..
/* device objects for: 保存设备指针的全局变量 */
PDEVICE_OBJECT
    g_tcpfltobj = NULL,        // \Device\Tcp
    g_udpfltobj = NULL,        // \Device\Udp
    g_ipfltobj = NULL,        // \Device\RawIp
    g_devcontrol = NULL,    // control device (exclusive access only!)
    g_devnfo = NULL;        // information device 

..
/* deinitialization 对已生成和绑定的设备进行解绑和删除 */
VOID
OnUnload(IN PDRIVER_OBJECT DriverObject)
{
    // Add by tan wen. 
    tdifw_driver_unload(DriverObject);

#ifndef USE_TDI_HOOKING
    d_n_d_device(DriverObject, g_tcpoldobj, g_tcpfltobj);
    d_n_d_device(DriverObject, g_udpoldobj, g_udpfltobj);
    d_n_d_device(DriverObject, g_ipoldobj, g_ipfltobj);
#else
    if (g_hooked)
        hook_tcpip(&g_old_DriverObject, FALSE);  
#endif

    // delete control device and symbolic link
    if (g_devcontrol != NULL) {
        UNICODE_STRING linkname;
        
        RtlInitUnicodeString(&linkname, L"\\??\\tdifw");
        IoDeleteSymbolicLink(&linkname);

        IoDeleteDevice(g_devcontrol);
    }

    // delete info device and symbolic link
    if (g_devnfo != NULL) {
        UNICODE_STRING linkname;
        
        RtlInitUnicodeString(&linkname, L"\\??\\tdifw_nfo");
        IoDeleteSymbolicLink(&linkname);

        IoDeleteDevice(g_devnfo);
    }

    filter_free();
    ot_free();
    conn_state_free();        // call after ot_free()

    memtrack_free();

}
..
extern NTSTATUS
tdifw_driver_entry(
            IN PDRIVER_OBJECT theDriverObject,
            IN PUNICODE_STRING theRegistryPath);

/* initialization */
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT theDriverObject,
            IN PUNICODE_STRING theRegistryPath)
{
    NTSTATUS status = STATUS_SUCCESS;
    int i;
    UNICODE_STRING name, linkname;

    if(status != STATUS_SUCCESS)
        goto done;

    memtrack_init();
    KeInitializeSpinLock(&g_traffic_guard);

#ifdef USE_TDI_HOOKING
    KdPrint(("[tdi_fw] WARNING! Using unstable working mode: TDI hooking!\n"));
#endif

    status = ot_init();
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: ot_init: 0x%x\n", status));
        goto done;
    }

    status = filter_init();
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: filter_init: 0x%x\n", status));
        goto done;
    }

    status = conn_state_init();
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: conn_state_init: 0x%x\n", status));
        goto done;
    }
    
    //设置分发函数
    for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
        theDriverObject->MajorFunction[i] = DeviceDispatch;

#if DBG
    // register UnLoad procedure
    theDriverObject->DriverUnload = OnUnload;
#endif

    /* create control device and symbolic link */

    RtlInitUnicodeString(&name, L"\\Device\\tdifw");

    status = IoCreateDevice(theDriverObject,
                            0,
                            &name,
                            0,
                            0,
                            TRUE,        // exclusive!
                            &g_devcontrol);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: IoCreateDevice(control): 0x%x!\n", status));
        goto done;
    }

    RtlInitUnicodeString(&linkname, L"\\??\\tdifw");

    status = IoCreateSymbolicLink(&linkname, &name);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: IoCreateSymbolicLink: 0x%x!\n", status));
        goto done;
    }

    RtlInitUnicodeString(&name, L"\\Device\\tdifw_nfo");

    status = IoCreateDevice(theDriverObject,
                            0,
                            &name,
                            0,
                            0,
                            FALSE,        // not exclusive!
                            &g_devnfo);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: IoCreateDevice(nfo): 0x%x!\n", status));
        goto done;
    }

    RtlInitUnicodeString(&linkname, L"\\??\\tdifw_nfo");

    status = IoCreateSymbolicLink(&linkname, &name);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: IoCreateSymbolicLink: 0x%x!\n", status));
        goto done;
    }

#ifndef USE_TDI_HOOKING

    // Add by tanwen.
    // Call this function before hooking! So that when tdifw_filter() happened, 
    // Our driver has been initialized.
    status = tdifw_driver_entry(theDriverObject,theRegistryPath);

    //生成过滤设备绑定TCP设备
    status = c_n_a_device(theDriverObject, &g_tcpfltobj, &g_tcpoldobj, L"\\Device\\Tcp");
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: c_n_a_device: 0x%x\n", status));
        goto done;
    }
    //生成过滤设备绑定UDP设备
    status = c_n_a_device(theDriverObject, &g_udpfltobj, &g_udpoldobj, L"\\Device\\Udp");
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: c_n_a_device: 0x%x\n", status));
        goto done;
    }
    //生成过滤设备绑定RawIp设备
    status = c_n_a_device(theDriverObject, &g_ipfltobj, &g_ipoldobj, L"\\Device\\RawIp");
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: c_n_a_device: 0x%x\n", status));
        goto done;
    }

#else    /* USE_TDI_HOOKING */

    // Add by tanwen.
    // Call this function before hooking! So that when tdifw_filter() happened, 
    // Our driver has been initialized.
    status = tdifw_driver_entry(theDriverObject,theRegistryPath);

    /* get device objects for tcp/udp/ip */

    status = get_device_object(L"\\Device\\Tcp", &g_tcpfltobj);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: get_device_object(tcp): 0x%x\n", status));
        goto done;
    }
    
    status = get_device_object(L"\\Device\\Udp", &g_udpfltobj);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: get_device_object(udp): 0x%x\n", status));
        goto done;
    }
    
    status = get_device_object(L"\\Device\\RawIp", &g_ipfltobj);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: get_device_object(ip): 0x%x\n", status));
        goto done;
    }

    /* hook tcpip */

    status = hook_tcpip(&g_old_DriverObject, TRUE);
    if (status != STATUS_SUCCESS) {
        KdPrint(("[tdi_fw] DriverEntry: hook_driver: 0x%x\n", status));
        goto done;
    }
    g_hooked = TRUE;

#endif    /* USE_TDI_HOOKING */

    status = STATUS_SUCCESS;

done:
    if (status != STATUS_SUCCESS) {
        // cleanup
        OnUnload(theDriverObject);
    }

    return status;
}

0x4: 主要过滤的请求类型

我们已经了解了TDI过滤框架,接下来继续讨论应该过滤什么请求,假设作为一个防火墙或者一个安全监控软件,希望监控当前计算机内的软件对网络进行的访问,例如连接请求、连接IP地址、端口等信息,我们必须进一步了解TDI接口的请求方式
所有的请求(IRP)都在DeviceDispatch中处理,IRP已主功能号和次功能号决定它的类型
TDI接口的调用总是遵循着: "打开->设备控制->关闭"的流程,我们将截取这些请求中的信息,来进行过滤

0x5: code example

///
/// @file         tdifw_smpl.c
/// @author    crazy_chu
/// @date       2009-4-11
/// @brief      使用tdi_fw.lib的一个例子。 
/// 
/// 免责声明
/// 本代码为示例代码。未经详尽测试,不保证可靠性。作者对
/// 任何人使用此代码导致的直接和间接损失不负责任。
/// 

#include "..\inc\tdi_fw\tdi_fw_lib.h"

NTSTATUS
tdifw_driver_entry(
            IN PDRIVER_OBJECT theDriverObject,
            IN PUNICODE_STRING theRegistryPath)
{
    // 直接返回成功即可。
    return STATUS_SUCCESS;
}

VOID
tdifw_driver_unload(
            IN PDRIVER_OBJECT DriverObject)
{
    // 没有资源需要释放。
    return;
}

NTSTATUS tdifw_user_device_dispatch(
    IN PDEVICE_OBJECT DeviceObject, IN PIRP irp)
{
    // 不会有任何请求到达这里。我们没有注册过自定义设备。
    return STATUS_UNSUCCESSFUL;
}

u_short
tdifw_ntohs (u_short netshort)
{
    u_short result = 0;
    ((char *)&result)[0] = ((char *)&netshort)[1];
    ((char *)&result)[1] = ((char *)&netshort)[0];
    return result;
}


int tdifw_filter(struct flt_request *request)
{
    if(request->proto == IPPROTO_TCP)
    {
        struct sockaddr_in* from = (struct sockaddr_in*)&request->addr.from;
        struct sockaddr_in* to = (struct sockaddr_in*)&request->addr.to;

        // 然后打印协议类型
        DbgPrint("tdifw_smpl: protocol type = TCP\r\n");
        // 打印当前进程的PID。
        DbgPrint("tdifw_smpl: currect process = %d\r\n",request->pid);

        // 打印事件类型
        switch(request->type)
        {
        case TYPE_CONNECT:
            DbgPrint("tdifw_smpl: event: CONNECT\r\n");
            break;
        case TYPE_DATAGRAM:
            DbgPrint("tdifw_smpl: event: DATAGRAM\r\n");
            break;
        case TYPE_CONNECT_ERROR:
            DbgPrint("tdifw_smpl: event: CONNECT ERROR\r\n");
            break;
        case TYPE_LISTEN:
            DbgPrint("tdifw_smpl: event: LISTEN\r\n");
            break;
        case TYPE_NOT_LISTEN:
            DbgPrint("tdifw_smpl: event: NOT LISTEN\r\n");
            break;
        case TYPE_CONNECT_CANCELED:
            DbgPrint("tdifw_smpl: event: CONNECT CANCELED\r\n");
            break;
        case TYPE_CONNECT_RESET:
            DbgPrint("tdifw_smpl: event: CONNECT RESET\r\n");
            break;
        case TYPE_CONNECT_TIMEOUT:
            DbgPrint("tdifw_smpl: event: CONNECT TIMEOUT\r\n");
            break;
        case TYPE_CONNECT_UNREACH:
            DbgPrint("tdifw_smpl: event: CONNECT UNREACH\r\n");
            break;
        default:
            break;
        }
  

        // 如果是TCP,我们打印更多的内容。包括方向,来源IP地址
        // 目的IP地址,等等。但是对于其他协议就不打印了。
        DbgPrint("tdifw_smpl: direction = %d\r\n",request->direction);
        DbgPrint("tdifw_smpl: src port = %d\r\n",tdifw_ntohs(from->sin_port));
        DbgPrint("tdifw_smpl: src ip = %d.%d.%d.%d\r\n",
            from->sin_addr.S_un.S_un_b.s_b1,
            from->sin_addr.S_un.S_un_b.s_b2,
            from->sin_addr.S_un.S_un_b.s_b3,
            from->sin_addr.S_un.S_un_b.s_b4);
        DbgPrint("tdifw_smpl: dst port = %d\r\n",tdifw_ntohs(to->sin_port));
        DbgPrint("tdifw_smpl: dst ip = %d.%d.%d.%d\r\n",
            to->sin_addr.S_un.S_un_b.s_b1,
            to->sin_addr.S_un.S_un_b.s_b2,
            to->sin_addr.S_un.S_un_b.s_b3,
            to->sin_addr.S_un.S_un_b.s_b4);
    }

    return FILTER_ALLOW;
}

Relevant Link:

http://blog.csdn.net/charlesprince/article/details/5924376
http://www.cnblogs.com/welfear/archive/2011/02/14/1954454.html
http://wenku.baidu.com/view/b9d481c758f5f61fb736663b.html

 

8. WFP(Windows Filtering Platform windows过滤平台)

Relevant Link:

http://os.51cto.com/art/200912/167639.htm 
https://msdn.microsoft.com/en-us/library/windows/hardware/dn653358(v=vs.85).aspx
http://blog.csdn.net/a809146548/article/details/45028871

 

Copyright (c) 2015 LittleHann All rights reserved

 

posted @ 2015-10-12 20:03  郑瀚Andrew  阅读(15253)  评论(0编辑  收藏  举报