Windows驱动开发入门-NDIS协议驱动
Windows驱动开发入门-NDIS协议驱动
碎碎念
本节代码需要最低警告、关警告按错误处理、关符合模式、关SDL检查、导入ndis.lib和wdmsec.lib。可直接参考Github上微软官方的驱动代码示例库的ndisprot工程。本节代码用传统型驱动编码方式,后面NDIS小端口驱动将用WDF驱动编码方式。
最常用的Ethernet V2(ARPA)以太网包如下,若类型2字节为0x80、0x00则表示为IP包。
低地址 | 高地址 | ||
---|---|---|---|
源网卡MAC地址 | 目标网卡MAC地址 | 类型 | 数据 |
6字节 | 6字节 | 2字节 | 其他 |
协议驱动接收上层用户的Socket请求,把这些数据封装为IP包,再把IP包封装成以太网包发送出去;接收到以太网包时,分析这时给哪个用户程序的,把用户数据解析出来,提交给上层应用程序。协议驱动较多用于嗅探,如Wincap,一般不用于防火墙,应为这玩意儿不好干预应用程序发送或接收包。
网卡驱动接口标准NDIS是一组定义好的函数接口的集合。NDIS网络驱动有三种:协议驱动、小端口驱动、中间层驱动(包含过滤驱动),开发者可提供这三种不同的内核模块给NDIS使用。协议驱动上层提供直接供应用层Socket使用的数据传输接口,下层绑定小端口,用于发送与接收以太网包。小端口驱动针对网卡,给协议层提供接收和发送数据包的能力。传统中间层驱动即将被过滤驱动淘汰,在协议驱动和小端口驱动之间。
开发历程为:填写协议特征(协议回调函数列表);把自己注册为协议驱动;系统对每个实际存在的网卡实例调用本协议驱动在协议特征集中提供的一个回调函数,在该回调函数中决定是否要绑定一个网卡,一旦绑定,该网卡接收到的包将提交给该协议驱动,后者也可用该网卡发送包;发生各种事件时,如网卡接收到一个新数据包,特征集中某个函数被调用,后者决定如何处理接收到的数据包;当应用层试图发送一个以太网包时,可打开该协议并发出请求。
协议驱动入口
要做的有:生成一个控制设备和一个符号链接,指定分发函数;注册一个协议,提供协议特征。入口在ntdisp.c中。
1 | NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) |
协议与网卡绑定
当一个协议驱动绑定了一个网卡时,网卡收到的数据包会提交给该协议,协议可用该网卡发送数据包。网卡也可以是虚拟网卡。本小节详细讲NdisProtBindAdapter
内部工作原理,想直接开发的就跳过本小节。
协议驱动的打开上下文:打开指的是打开网卡,也就是绑定网卡,NDIS中用NdisOpenAdapter
。该驱动中每当有一个网卡被绑定,就分配一个内存空间来保存和这次绑定相关的信息,以及本次绑定所需要用到的资源,如锁和队列等。
NdisProtBindAdapter
工作有:打开上下文的分配和初始化;将打开的上下文保存到全局链表中,完成绑定。代码在ndisbind.c中。
1 | VOID NdisProtBindAdapter(OUT PNDIS_STATUS pStatus, IN NDIS_HANDLE BindContext, IN PNDIS_STRING pDeviceName, IN PVOID SystemSpecific1, IN PVOID SystemSpecific2) |
协议绑定网卡用NdisOpenAdapter
,原型如下。
1 | VOID NdisOpenAdapter( |
ndisprotCreateBinding
工作有:防止多线程竞争;分配和初始化本次绑定的相关资源;获得网卡参数,代码如下。
自旋锁会提高中断级别,在此之后很多事情就不能做了。自旋锁非常消耗资源,在锁住时会不断重试,此时不宜做耗时长、复杂的操作。
需要分配的资源有输入和输出缓冲区的包池和缓冲池。包池是一组预先分配好的包描述符,缓冲池是一组已分配好的包缓冲区描述符。每个以太网包都用一个NDIS_PACKET包描述符来描述,实际内容包含在NDIS_BUFFER包缓冲区描述符。网卡接收到包后要保存到报池中等待上层应用程序取走,上层应用程序要求发送的包先保存在包池中再发送出去。这样在具体有以太网包需要存入时,不用再次分配包描述符和包缓冲区描述符,以此减少动态分配内存的消耗。
NDIS对象描述符OID是一组系统定义的常熟,每个OID为一个请求的类型号。NDIS上层驱动可以给下层驱动发送OID请求,下层驱动必需返回约定的结果。如可以获取网卡MAC地址、最大帧长等。
1 | NDIS_STATUS ndisprotCreateBinding(IN PNDISPROT_OPEN_CONTEXT pOpenContext,__in_bcount(BindingInfoLength) IN PUCHAR pBindingInfo,IN ULONG BindingInfoLength) { |
上述ndisprotDoRequest
函数封装了NdisRequest
,所有OID都用它发送,原型如下:
1 | VOID NdisRequest( |
至于请求句柄咋填:
1 | NDIS_STATUS ndisprotDoRequest(IN PNDISPROT_OPEN_CONTEXT pOpenContext, IN NDIS_REQUEST_TYPE RequestType, IN NDIS_OID Oid, IN PVOID InformationBuffer, IN ULONG InformationBufferLength, OUT PULONG pBytesProcessed) { |
完成函数的编写:
1 | VOID NdisProtRequestComplete(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_REQUEST pNdisRequest, IN NDIS_STATUS Status) { |
解除绑定
当一个网卡被拔出时,内核调用协议特征集中的解除绑定回调函数来解除一个协议驱动和一个网卡的绑定。解除绑定用NdisCloseAdapter
,原型如下。
1 | VOID NdisCloseAdapter( |
NdisProtUnbindAdapter
实现:
1 | VOID NdisProtUnbindAdapter(OUT PNDIS_STATUS pStatus, IN NDIS_HANDLE ProtocolBindingContext, IN NDIS_HANDLE UnbindContext) |
其中ndisprotShutdownBinding
的实现如下:
1 | VOID ndisprotShutdownBinding(IN PNDISPROT_OPEN_CONTEXT pOpenContext) { |
用户态操作协议驱动
本小节为应用层程序代码。
标准的协议驱动可在用户层用Socket打开进行收发包的,但本例中抛弃了上层接口,简单地用ReadFile
和WriteFile
进行收发包。ReadFile
从应用层发出一个控制请求到某个设备,主功能号IRP_MJ_READ,WriteFile
同理。DeviceIoControl
调用时,设备收到一个控制请求,IRP主功能号IRP_MJ_DEVICE_CONTROL。
应用层基本步骤为:用CreateFile
打开协议控制设备对象CDO,得到其句柄;用DeviceIoControl
来等待绑定结束;用WriteFile
发送数据包,用ReadFile
接收数据包;用CloseHandle
关闭句柄。本例驱动中设置的符号链接为“\\.\NdisProt”。
打开设备并发送控制请求:
1 | HANDLE OpenHandle( |
其中DeviceIoControl
原型:
1 | BOOL DeviceIoControl( |
但此时仅用CreateFile
打开了一个句柄,并输入了协议驱动的符号链接,还需要用DeviceIoControl
传入设备名,控制码IOCTL_NDISPROT_OPEN_DEVICE。
1 | WCHAR wNdisDeviceName[MAX_NDIS_DEVICE_NAME_LEN]; |
接下来用WriteFile
发送数据包,需要填写数据区和以太网包头。以太网包头有一个来源地址、一个目的地址、一个协议号。来源地址可以冒名顶替。
1 | pWriteBuf = malloc(PacketLength); |
WriteFile
和ReadFile
原型为:
1 | BOOL WriteFile( |
读取数据包方法:
1 | PUCHAR pReadBuf = NULL; |
内核态功能实现
在驱动入口函数有设置各类分发函数。分发函数NdisProtIoControl
处理所有主功能号为IRP_MJ_DEVICE_CONTROL的IRP。
1 | NTSTATUS NdisProtIoControl(IN PDEVICE_OBJECT pDeviceObject,IN PIRP pIrp) |
ndisprotOpenDevice
实现步骤有:从输入缓冲区拿到设备名、通过设备名去找对应的打开上下文、找到后保存在pIrpSp->FileObject->FsContext
。在文件系统中FsContext保存FCB,但其他驱动中可以提供“文件”对象。
1 | NTSTATUS ndisprotOpenDevice( |
读请求就是从应用层获取网卡收到的包,这些包被本协议驱动放入缓冲队列中。处理读请求就是检测队列中有无数据包,有则把包内容拷贝到读请求输出缓冲区中。若存在多个网卡,则把打开上下文取出,就能知道调用者需要收包的是哪个网卡。
1 | // 分发函数之一,处理读请求。 |
写请求就是发包请求,处理代码在NdisProtWrite
中,主请求号IRP_MJ_WRITE。
1 | // 分发函数,处理写请求(也就是发包请求)。 |
包发送NdisProtSendComplete
的实现:
1 | // 这是协议特征集中的一个回调函数。如果调用了NdisSendPacket,那么在发送结束之后,这个函数会被调用。 |
协议驱动接收回调
当被绑定的网卡收到数据包时,内核调用ReceiveHandler
和ReceivePacketHandler
,至于啥时候调用哪个尚不明朗。ReceiveCompleteHandler
回调函数在ReceiveHandler
调用完后。若只需要包的数据内容的前几字节就可决定该包是否是本协议需要处理的,那么下层驱动就没必要提交整个包,只提供包开始的几个字节给协议驱动,这称为前视区。若决定获取完整数据,应用NdisTransferData
,传输完成后回调函数TransferDataCompleteHandler
将被调用。
1 | NDIS_STATUS NdisProtReceive( |
另一种类型的接收回调函数指针ReceivePacketHandler
的原型为:
1 | INT NdisProtReceivePacket( |
上述TransferDataCompleteHandler
实现如下:
1 | VOID NdisProtTransferDataComplete(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_PACKET pNdisPacket, IN NDIS_STATUS TransferStatus, IN UINT BytesTransferred) |
这里ReveicePacketHandler
返回值为引用计数,即接收到的包描述符被本协议驱动使用的次数。若包被本驱动使用,则下层网卡不能释放这个包。引用计数为0才能释放。若要重用该网络包,就要返回一个引用计数,一般1就行。下层驱动资源紧张时,必需释放该包并重新分配,拷贝到新的包描述符中。
1 | INT NdisProtReceivePacket(IN NDIS_HANDLE ProtocolBindingContext, IN PNDIS_PACKET pNdisPacket) |
上面这俩接收包的回调函数最终都是收到一个数据包并把它放入队列中。该队列用LIST_ENTRY实现,还有一些常用宏:
1 |
ndisprotQueueReceivePacket
代码实现如下。
1 | VOID ndisprotQueueReceivePacket(IN PNDISPROT_OPEN_CONTEXT pOpenContext, IN PNDIS_PACKET pRcvPacket) |
上述ndisprotServiceReads
负责接收数据包的出队和读请求的完成。该函数当读请求队列和接收包队列都不为空时,从包中取得数据,拷贝到IRP里,完成该IRP。pOpenContext->PendedReads
为未决读请求队列,pOpenContext->RecvPktQueue
为接收包缓冲队列,这俩实际是链表。
1 | VOID ndisprotServiceReads(IN PNDISPROT_OPEN_CONTEXT pOpenContext) |