TDI Filter 过滤驱动

By Fanxiushu  2013, 引用和转载请注明原作者

为了让大家有兴趣阅读下去,
举个正在使用的可能大家都比较熟悉的例子: 360 的安全卫士里,有个流量防火墙的功能,
它可以监视每个进程的流量情况,可以限制上传下载速度,等等。
他的驱动部分的就是一个 TDI Filter 驱动。

TDI Filter ,这是个快被微软淘汰的驱动模式,但是为了兼容,又不得不使用的驱动。
是因为新的Windows系统中,有了更简单的开发框架替代TDI框架。
到某天XP系统如win98,win2000那样消失的时候,TDI功能会被微软从新的系统中去除掉,到时就真正不能使用TDI做开发了。
如果你的应用要能兼容XP,WIN7,WIN8,而且又不想写两套代码来分别应付两套不同的系统,那使用TDI是最好的选择。
如果你的应用只在WIN7 以上的系统里运行,可以使用WFP代替TDI Filter。
至于说win7以上系统TDI效率比较差的问题,你又不是做服务器,个人PC机以现在的硬件能力,这点损失基本忽略不计。

据说WFP是比TDI Filter简单,实际上微软的东西都是那么臃肿,能简单到哪去呢;
WIN7以上又出来一个WDF框架,类似应用层的MFC框架一样,把底层代码封装了一遍,
作为一个初学者或者想深入理解内核的工程师,我觉得还是应从WDM基础和原理上去研究。

关于TDI Filter驱动,网上介绍比较多。还有开源工程tdifw,更详细的从代码上述说了TDI Filter的开发细节。
很多时候,这个东西是用来做网络防火墙的,就是可以阻止你想要阻止的网络连接,
让不想访问的数据包不进入到你的电脑里,同时可以实时监控网络流量,限制流量,修改数据包等等。

TDI Filter是标准的NT式设备过滤驱动,所以按照标准的NT式过滤驱动模式来开发TDI就是最正确的了。

首先在 DriverEntry里 替换掉所有的派遣函数为自己的函数,如下
for( int i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
       DriverObject->MajorFunction[i] = myDispatch;

接着调用IoCreateDevice创建设备,然后调用 IoAttachDevice 挂载到 \Device\Tcp和Device\Udp等 TDI 标准设备上,
若有必要再创建一个控制设备,用来跟用户程序通讯。
这样一个TDI Filter 就初始化成功了,接着主要的任务就是 在 myDispatch 派遣函数里处理具体的任务了。
我们最关心的其实就3个MAJOR命令:
IRP_MJ_CREATE                                          //创建地址对象和连接对象
IRP_MJ_CLEANUP                                       //销毁地址对象和连接对象
IRP_MJ_INTERNAL_DEVICE_CONTROL     //发送网络数据传输等命令

简单来说 TDI Filter 就是对以上三个命令进行过滤,但是最后一个命令牵涉到的子命令很多,所以整个的过滤就比较臃肿了。
下文所说的 依照 TDI 开头的命令,都是 IRP_MJ_INTERNAL_DEVICE_CONTROL 的子命令。

伪代码如下:
NTSTATUS DriverEntry(.....)
{
       for(int i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
             DriverObject->MajorFunction[i] = myDispatch;
      
       创建TCP过滤设备并挂载到 标准 的TDI的TCP
       IoCeateDevice( .... &tcp_filter_dev,...); 
       IoAttachDevice( tcp_filter_dev, "\\Device\\Tcp",... );
      
      创建UDP过滤设备,并挂载到 标准TDI的UDP
      IoCreateDevice( ...., &udp_filter_dev, ... );
      IoAttachDevice( udp_filter_dev, "\\Device\\Udp", ... );
       
      若有必要,创建一个控制设备
      IoCreateDevice( ... &control_dev, ... ); 
    .............
}
NTSTATUS myDispatch( PDEVICE_OBJECT dev, PIRP irp)
{
     irpStack = IoGetCurrentIrpStackLocation( irp);
     /
     switch( irpStack->MajorFunction)
     {
     case IRP_MJ_CREATE:
           ....
           break;
    case IRP_MJ_CLEANUP:
          .....
          break;
    case IRP_MJ_INTERNAL_DEVICE_CONTROL:
         .....
         break;
     }
     ...............
    
}

应用层的网络数据包,绝大部分都是TCP和UDP协议的,所有要通讯的程序,进入到Windows内核,都会调用afd.sys驱动,
afd.sys驱动管理所有应用层的套接字,他是TDI的客户端,
比如我们在程序中调用socket和connect等函数进行TCP通讯时,afd.sys驱动会打开TDI的TCP设备,
创建至少两个对象:一个地址对象,一个连接对象; 
地址对象绑定到本地某个未使用的地址(本地IP + 本地端口),
连接对象是用来跟远端机器建立起一对一的连接,以后在数据通讯中,都使用这个连接对象进行数据的收发工作。
当两个对象创建好之后,afd.sys接着就会把连接对象跟本地地址对象关联起来(TDI_ASSOCIATE_ADDRESS ),
这样才能知道这个连接对象是使用这个本地地址跟远端机器通讯。
关联好之后,接着就发送连接命令(TDI_CONNECT),成功之后,就可以使用这个连接对象进行数据收发工作了。
以上说的是 TCP协议中,作为客户端的机器的工作情况。
TCP协议中,作为服务端的工作情况,稍微有点差别,
应用程序调用socket,bind,listen之后,afd.sys驱动依然会首先创建地址对象, 
接着会创建 N 个连接对象,这个N一般是listen函数的参数。
然后依然需要把连接对象跟地址对象关联起来。
接着就等待 TDI_EVENT_CONNECT 事件,当有客户端连上来之后,这事件会被相应,于是连接成功后,
接着就跟客户端那样用连接对象收发数据了。

在创建连接对象时候,还必须把他的一个参数:连接上下文(其实就是一个指针值)保存起来,
这样我们在收数据包(实际是接收事件中)的时候,才能根据这个连接上下文找到正确的连接对象。

至于UDP通讯,那就简单多了,他不存在连接的概念,所以不需要连接对象,
afd.sys驱动只创建一个地址对象,然后就用这个地址对象收发数据包。

接着看看收发数据包的命令,
发送数据包比较简单点:
在TCP中,使用 TDI_SEND 命令发送,
在UDP中,使用 TDI_SEND_DATAGRAM命令发送。
也没什么特别,只需按照一般的过滤驱动模式处理这些命令即可。

收数据包就比较复杂了,他除了提供TDI_RECEIVE和TDI_RECEIVE_DATAGRAM命令之外,
还提供事件回调函数处理接收操作,
要能抓取所有接收的数据包,我们必须对这些事件进行处理。
所谓的事件回调,是afd.sys驱动会发送一个 TDI_SET_EVENT_HANDLER命令下来,
并提供某些事件的回调函数地址
告诉 tcpip.sys 协议驱动说,我打算利用这些地址来接收数据,当你有数据的时候,就调用这些回调函数告诉给我。
如果回调函数里的数据还不能完全满足我,我就接着发送 TDI_RECEIVE 请求更多的数据。

TDI filter过滤驱动一般就在 TDI_SET_EVENT_HANDLER命令中,保存上层的回调函数地址,然后用我们自己的地址替换掉,
当tcpip.sys有数据就会调用我们过滤驱动里的回调函数,我们做些处理,然后接着调用我们保存的上层的回调函数地址。
TCP对应的事件我们一般感兴趣的如下:
TDI_EVENT_CONNECT                //连接 事件,这是作为服务器端的TCP相应的事件,当有客户端连上来,此事件回调函数会被tcpip.sys调用
TDI_EVENT_DISCONNECT          //同上,这是断开连接的事件
TDI_EVENT_RECEIVE                 //接收事件,当有数据到来时候,tcpip.sys会调用此事件的回调函数
TDI_EVENT_RECEIVE_EXPEDITED   //这个是 OOB,就是TCP概念中的紧急带外数据,依然是接收事件
TDI_EVENT_CHAINED_RECEIVE       //这个是 只读接收事件,就是 客户端可以一次性读取的数据。其实具体有何用处,我也不太清楚,反正也必须得处理
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED  //这个是只读事件的带外数据事件。

UDP的事件也比这简单多,主要是两个
TDI_EVENT_RECEIVE_DATAGRAM
TDI_EVENT_CHAINED_RECEIVE_DATAGRAM
意思同TCP的差不多,不过第2个事件,我们其实都可以懒得去处理。


总结一下以上的我们必须处理的子命令:
TCP
关联连接对象和地址对象:TDI_ASSOCIATE_ADDRESS,TDI_DISASSOCIATE_ADDRESS
主动连接:        TDI_CONNECT, TDI_DISCONNECT
被动连接事件: TDI_EVENT_CONNECT, TDI_EVENT_DISCONNECT
发送 :              TDI_SEND
接收:               TDI_RECEIVE
接收事件:        TDI_EVENT_CONNECT,TDI_EVENT_DISCONNECT, TDI_EVENT_RECEIVE,TDI_EVENT_RECEIVE_EXPEDITED,
TDI_EVENT_CHAINED_RECEIVE, TDI_EVENT_CHAINED_RECEIVE_EXPEDITED

UDP
发送:        TDI_SEND_DATAGRAM
接收:        TDI_RECEIVE_DATAGRAM
接收事件: TDI_EVENT_RECEIVE_DATAGRAM,TDI_EVENT_CHAINED_RECEIVE_DATAGRAM

TCP和UDP都必须处理的命令:
设置事件的命令: TDI_SET_EVENT_HANDLER
还有主命令: IRP_MJ_CREATE, IRP_MJ_CLEANUP

其实算算也不算太多,复杂的是TCP。

接着说说如何获得哪些进程占用和释放了哪些TCP和UDP本地端口,(这是我比较感兴趣的)。
主要是对 IRP_MJ_CREATE命令的处理,
上面已经说了,afd.sys驱动会首先创建地址对象,就是为了绑定到本地的(IP+PORT)地址,
就是在这个命令中获得每个套接字绑定到的本地端口。
在此命令中 直接调用 PsGetCurrentProcessId  即可获得这个套接字所在的用户进程。
上层创建 地址对象和连接对象时候,会有一个FILE_FULL_EA_INFORMATION 结构的参数保存到
IRP->AssociatedIrp.SystemBuffer 里,我们取出这个参数,进行判断,
FILE_FULL_EA_INFORMATION* ea = (FILE_FULL_EA_INFORMATION*)IRP->AssociatedIrp.SystemBuffer;
如上,如果ea为空,说明创建的是其他对象,而不是地址对象和连接对象,(afd.sys有可能还会创建控制对象)
如果不为空,可对 ea进行判断,从而知道上层创建的究竟是地址对象,还是连接对象。
如果判断出是地址对象,必须要等到这个IRP完成之后,才能正确获得本地端口。
于是设置完成函数,在完成函数里,调用TDI_QUERY_INFORMATION查询这个地址对象绑定到的本地端口。
原理不复杂,但是处理细节挺讨厌的。
有兴趣可仔细看看我在工程的处理细节。

最后简单说说,如何监控每个进程的流量已经每个进程每个连接的流量,以及简单阻止进程访问网络。
要做到如上几点,我们必须用一个结构来保存 地址对象和连接对象,同时要能快速的查找和删除。
同时也要保存 进程ID和进程相关的结构。
我使用的是 rbtree.c和rbtree.h (linux内核中的红黑树源码)。
每个对象都可以在IRP_MJ_CTRAETE命令处理中获得进程ID,
我们其实就是在IRP_MJ_CREATE命令中构造一个以创建的对象为key的结构,
然后把相关的所以信息填写进去(包括进程ID,流量初始化,等等)。
在 IRP_MJ_CLEANUP中,再删除这个对象对应的结构。
在 TCP的连接命令和事件中,通过判断进程是否需要禁止访问网络,从而决定是否建立此连接。
在接收命令和事件中,通过连接对象和连接上下文(或者UDP中只有地址对象)来找到我们的结构,
通过此结构找到进程相关的结构,然后把流量累加上去。
发送也是如此。

http://download.csdn.net/detail/fanxiushu/5198130

上面的连接是对应的TDI工程,是借用 tdifw的思想,重新搭建的TDI Filter框架
如果您对这代码不感兴趣,可直接查阅 tdifw开源工程。

附:(直到2013-04-09,大致发现以上连接的工程的以下BUG,以此作为记录,方便查询)

1,在 xioctl.cpp源文件中,xi_done_laddr_irps的函数,不管成功与失败,都应该返回 STATUS_SUCCESS,否则应用程序不能获得全部的端口变化通知。

2,还是在 xi_done_laddrirps函数里,在调用IoSetCancelRoutine之前应该 InitializeListHead( &irp->Tail.Overlay.ListEntry ); 否则可能会 BSOD。

3,xfilter.cpp代码里调用xf_set_event_handler设置事件地址:

上层取消此次事件,直接设置evt->old_handler为空,但是还没把命令下传给底层驱动,

很可能在这空档时间内,底层驱动继续调用回调函数,所以在所有事件中,必须对old_handler是否为空进行判断,否则可能会BSOD。

4,应该在 IRP_MJ_CLEANUP的完成函数里删除 obj_t对象结构,否则如上那样,容易在底层还么关闭期间,而obj_t结构删除了,驱动访问无效地址而BSOD。

5, xf_recv.cpp源文件的xf_event_chain_receive函数不能简单返回 STATUS_DATA_NOT_ACCEPTED了事,要调用原来的回调函数,否则可能会使上层接收不到数据.

6,xf_creat.cpp的完成函数里,在创建地址对象失败的时候,应该调用 tdi->xf.laddr.free(ctx) 释放先前创建的内存,否则时间长了,会内存泄漏从而造成BSOD。

7,xf_connect.cpp处理TCP连接,应该是成功后之后才查询本地地址,代码未在完成函数里做出错判断。


(到2013-04-12为止,大致发现这些BUG,工程就懒得更新了(主要是 CSDN没提供一个功能能够删除替换原来上传的代码),有兴趣的朋友可以帮忙找BUG,非常感谢)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值