NDIS深层网络封包截取研究
2012-10-17邹菊红
邹菊红
四川省水利职业技术学院 四川 611830
0 前言
随着计算机技术和通信技术的飞速发展,网络信息已渗透到社会生活的各个领域,随之而来的安全问题也日益严重,一旦网络安全问题发生,可能带来非常严重后果。由于互联网发展的历史原因,TCP/IP 协议及 HTTP、FTP 等基于 TCP/IP 协议的各种应用层协议,在协议设计之初均未考虑安全传输问题。随着互联网的发展,国际标准组织虽陆续推出了 SSL、HTTP1.1 等具有安全传输能力的应用层协议,但作为应用层承载协议的 TCP/IP 协议仍存在着固有的安全缺陷,造成至今未能有彻底的、低成本的、不需硬件支持的互联网安全传输解决方案。正是由于网络传输安全问题的现实存在,推动着黑客攻击技术、防火墙技术的不断发展。
1 NDIS的介绍
1.1 NDIS技术
Microsoft网络驱动程序接口规范(NDIS)的设计目的是通过将不同的协议从网络接口卡上拆除,使得用户可以访问不同的协议。在设计过程中,协议并不需要了解关于网络卡的任何信息。NDIS程序库(NDIS.sys)提供了一个面向NIC驱动程序的完全抽象的接口,网卡驱动程序与协议层驱动程序及操作系统通过这个接口进行通信。
网络封包截获,涉及驱动编程技术、核心态编程技术、系统动态链接库编程技术、协议生成与解析编程技术等,集中体现了网络应用的核心技术,是防火墙等高级网络应用开发的基础。基于Windows 2000和Windows XP的网络封包截获技术主要分为三种: WinSock2 动态链接库重载、传输层过滤驱动、中间层驱动。
1.2 动态数据库重载
WinSock2动态链接库重载:系统的WinScok2动态链接库,随系统启动而载入内存,提供 29个用于网络传输的功能函数。IE等普通上层应用程序,调用WinScok2动态链接库中的WSPSend、WSPRecv等函数,实现网络收、发功能。修改注册表中 HKEY_LOCAL_MACHINE/SYSTM/CURRENTCONTROLS ET/SERVICES/WinSock2项,建立新的自定义WSPStartup入口函数,可以重载winsock.dll。通过重载winsock.dll中的有关网络收、发函数,增加网络封包收、发前、后的自定义处理功能,实现网络封包截获。
1.3 传输层过滤驱动
传输层过滤驱动:使用 NDIS技术,又称 TDI编程(Transport Driver Interface传输层驱动接口编程)。Windows2000和WindowsXP中,TCP/IP协议作为系统驱动程序(../system32/TcpIp.sys),在系统启动时载入系统内存,以TCP/IP设备对象的形式供应用程序或其它系统程序调用。传输层过滤驱动程序创建一个或多个设备对象,直接挂接到TCP/IP设备对象之上。挂接成功后,当其它程序使用网络传输功能,调用TCP/IP设备对象时,操作系统首先将该调用映射到TCP/IP设备对象之上所挂接的传输层过滤驱动程序。通过传输层过滤驱动程序,再调用下层的TCP/IP设备对象,从而完成网络访问功能。同样,从TCP/IP层上传至应用程序的网络封包,也要经传输层过滤驱动程序后,再转发至目标应用程序端口。基于此工作原理,可以在传输层过滤驱动程序中实现网络封包截获,完成网络封包的过滤及加解密处理。
1.4 中间层驱动概述
中间层驱动:与传输层过滤驱动实现基本原理一致,也是使用 NDIS 技术。主要差别在于,中间层驱动程序,挂接在协议设备对象(包括 TCP/IP 设备对象)和网卡设备对象之间。任何进出网卡的网络封包,均必须首先经过中间层驱动程序的处理。从某种意义上分析,中间层驱动程序更像一个虚拟网卡。该虚拟卡封装了物理网卡,对物理网卡的一切网络访问操作,均必须先经虚拟卡处理。
2 基于NDIS深层封包截取的设计与实现
根据前面的介绍,本文采用VC++程序进行设计实现。
2.1 初始化
DriverEntry()函数是整个驱动程序的入口,它是在驱动程序被加载的时候由系统自动执行的。这个函数里面主要是进行一些驱动程序初始化的操作。
(1) 在Global结构里面记录函数传入的RegisterPath参数。
(2) 调用 IoCreateDevice为这个驱动程序创建一个DeviceObject,记录在Globals.ControlDeviceObject里面,这个DeviceObject是为了今后控制这个驱动程序用的(上层应用可以调用DeviceIoControl来对驱动程序的一些参数进行设置)。
(3) 为上面所建立的 DeviceObject建立 SymbolLink。它的作用是为了让上层的应用程序可以调用到所建立的DeviceObject(由于 Windows的层次关系,上层应用无法直接控制核心态的驱动程序,必须通过符号连接实现对核心的调用)。
(4) 逐一设置protocolChar结构中的每一个域。这些域一些是驱动程序所必需的版本号、名字等信息,另外一些是函数指针,它们指定了一些NDIS自动执行的函数入口地址。这里指定的每一个函数在程序中必须有它的具体实现。
(5) 调用NdisRegisterProtocol函数注册这个驱动程序,这个函数执行之后,这个驱动程序被加入到系统的设备列表中,以后的应用程序可以使用这个驱动程序了。
(6) 指定驱动程序的主功能(MajorFunction)代码处理函数。它是一个数组,主要指定在上层应用调用 CreateFile,ReadFile, WriteFile, DeviceIoControl和CloseHandle等函数的时候,下层驱动程序怎样处理。
(7) 指定驱动程序卸载时候的处理函数。
(8) 返回,如果遇到错误,进行错误处理。错误处理的主要内容就是判断错误出现的位置以及已经分配的资源,对已经分配的资源进行释放。
2.2 绑定
PacketBindAdapter()函数是驱动程序中必不可少的函数,它是操作系统自动调用的函数。在驱动程序加载的时候,操作系统自动查找当前机器上安装的网卡驱动程序,并对每一个网卡自动运行这个函数,其目的是在驱动程序中对每个网卡进行记录,以备以后的使用。因此,这个函数中的主要内容就是实现对网卡的登记注册。
(1) 从DeviceName中取得网卡的名字,并分配空间存储起来。
(2) 调用IoCreateDevice为这个网卡建立一个设备对象。
(3) 创建 deviceobject->deviceExtention 为 Open_Instance结构,用来记录这个设备一切信息。
(4) 为这个设备对象建立符号联接。
(5) 调用NdisAllocatePacketPool为这个设备分配包缓冲池。
(6) 初始化Open_Instance结构(open变量)的一些域。
(7) 调用NdisOpenAdapter完成这个设备的初始化工作。
(8) 设置Open_Instance结构中的一些域的初值。
(9) 把这个设备加入到Globals.AdapterList的队列中去。
(10) 错误处理。
2.3 I/O控制
实现对驱动程序以及它所绑定的设备的控制。究竟是对驱动程序本身还是对它绑定的设备是由传入的参数DeviceObject决定,如果它等于Global->ControlDeviceObject,那么它就是对驱动程序的控制,否则是对指定绑定设备的控制。程序开始,首先确定控制类型。由于用户态和核心态程序的信息交换是通过IRP来实现的,所以这个功能代码是存放在IRP的IO堆栈单元中。
2.3.1 枚举网络适配器
主要是通过PacketGetAdapterList函数来实现。这个功能在PacketIoControl函数中实现以下功能。
(1) 判断设备变量是否为全局控制设备变量,如不是,说明上层调用错误。返回错误信息。
(2) 调用PacketGetAdapterList寻找网络适配器列表,放到输出缓冲区中。
(3) 执行标准语句组完成IRP。
2.3.2 记录上层应用程序的Start按钮是否按下
这个功能主要通过修改设备结构中的变量Start来实现。
(1) 取设备信息结构变量。
(2) 判断Start域的值,根据要求修改。
(3) 完成IRP,返回。
2.3.3 设置缓冲区大小
这个功能的实现主要是通过修改设备结构中的变量bufsize来实现。
(1) 取设备信息结构变量。
(2) 取要设置的bufsize的值,修改open->bufsize。
(3) 完成IRP,返回。
2.3.4 重置适配器
这个功能主要是调用系统函数NdisReset来实现。不过在此之前,这个函数把当前的这个重置请求加入到了ResetIrpList中,这样做的目的主要是帮助PacketResetComplete函数找到IRP。
注意,当NDIS执行完Reset操作要完成IRP的时候,它必须知道是哪个IRP要求的Reset操作。而IRP是作为参数传到PacketIoControl函数中的,PacketResetComplete无法知道这个 IRP。这样就需要一个数据结构记录这个 IRP,这个数据结构就是ResetIrpList。在PacketIoControl函数中,把当前IRP加入到ResetIrpList中,然后执行NdisReset后马上返回。过了一段时间,Reset操作完成,NDIS自动调用PacketResetComplete函数,从ResetIrpList取出这个IRP,执行标准语句组,完成IRP。
2.3.5 实现NDIS请求
NDIS请求主要有两类。(1) 设置OID;(2) 查询OID。
这两个功能主要是通过调用系统函数NdisRequest实现。
(1) 从系统缓冲区中取出要进行操作的OID结构。
(2) 分配一个 pRequest变量,用来存储 IRP和当前的Request内容(OID的内容)。
(3) 判断OID的数据结构长度是否正确。
(4) 判断请求是设置OID还是查询OID,并针对相应的请求进行相应的变量设置。
(5) 调用 NdisRequest。
(6) 错误处理。
(7) 类似Reset请求,如果NdisRequest已经完成,那么驱动程序要显式的调用PacketRequestComplete函数完成IRP。
2.4 数据接收
自然的设计思想是每当上层应用程序请求读取数据时,就为该请求生成一个相应的IRP,并将此IRP置于一个读取等待队列之中。当NIC从网络上接收到数据包时,由NDIS负责调用相应的接收函数。接收函数从读取等待队列的首部取出一个 IRP,并将从网络上接收到的数据拷贝到由该 IRP所指定的缓冲区中。至此,上层应用程序便成功接收到了它所需要的数据。
这种接收方式的天生缺陷就是丢包。如果上层应用程序一直没有读取数据的请求,那么NIC从网络上接收到的数据就会直接被丢弃。
本程序的设计思路是协议驱动程序负责维护一个接收缓冲区,该缓冲区以队列的形式组织。当NIC通知NDIS已从网络上接收到数据包时,可以先将这些数据缓存起来。当上层应用程序需要读取数据时,该读操作的数据源不是直接从网络得到,而是经过有效管理的存放着数据的缓冲区。这样,丢包的问题便得以解决。
当然,如果上层应用程序还是一直没有读取数据的请求,或者上层应用程序处理数据的速度低于NIC从网络上接收数据的速度,再或者网络出现峰值流量时,缓冲区自然会逐渐被填满,最终还是会出现丢包的情况。但是在正常情况下,这种增设缓冲的方法已经足以避免丢包情况的发生了。
当NIC从网络上接收到数据并通知NDIS时,作为已经在 protocolChar结构中注册过的函数,NDIS将调用PacketReceiveIndicate或 PacketReceivePacket二者之一作为接收处理函数将NIC从网络上接收到的数据缓存起来。其中,PacketReceiveIndicate作为协议驱动程序的 ProtocolReceive函数是必须要提供的,而PacketReceivePacket作为协议驱动程序的ProtocolReceivePacket函数提供与否是可选的。二者的主要区别在于当 NIC支持一次接收多个数据包时使用ProtocolReceivePacket更富效率。
2.5 解码引擎接口实现
CPDEDecodeEngine提供解码所有字段、解码概要信息、解码字段,共三种方式解码接口。
以下引自CSPDEOBJ.h头文件中对该类的定义,提供给模块用户的主要功能。
2.6 解码引擎类的实现
3 结束语
本文对NDIS中间层驱动程序进行扩展研究,实现了对底层的网络数据包的截获,经过该项目的实际应用检验,结果表明,系统运行稳定,为信息安全和数据安全提供了有效的保障,对网络管理人员和网络安全有很大的帮助。使网络管理从原来的被动防御变成了现在的主动抵抗,从传统观念上也有了很大的改变,具有进一步研究的价值和广阔的应用前景。
[1]祁云嵩,刘永良,华伟.VC++程序设计解析与训练[M]].华东理工大学出版社.2005.
[2]孙鑫,余安萍.VC++深入详解[M].电子工业出版社.2006.
[3]辛长安,梅林编著.VC++编程技术与难点剖析[M].清华大学出版社.2002.
[4]张忠帅编著.VC++ 2008专题应用程序开发实例精讲[M].电子工业出版社.2008.
[5]宋金珂,高丽华,张迎新.VC++程序设计基础教程[M].清华大学出版社.2010.
[6]贾振华.VC++程序设计项目实践[M],清华大学出版社.2010.
[7]张惠娟.Windows环境下的设备驱动程序设计.西安电子科技大学出版社.2009.
[8]斯诺译.Windows2000设备驱动程序设计指南.北京:机械工业出版社.2006.
[9]李德全.拒绝服务攻击[M].电子工业出版社.2007.
[10]冀振燕.UML 系统分析与设计教程[M].人民邮电出版社.2010.
[11]李双华.C++编程思想[M].电子工业出版社.2009.
[12]陈宗斌等译,Michael J.Donahoo Kenneth L.Calvert著.TCP/IPSockets编程[M].清华大学出版社.2009.
[13]TCP-IP详解卷3:TCP事务协议,HTTP,NNTP和UNIX域协议.2009.