文章目录
TDI网络过滤驱动开发指南
TDI(Transport Driver Interface)其实是一种过时的网络过滤技术框架,在XP系统下面是非常主流的一种网络数据包过滤框架,如果你的软件需要在XP环境下面运行,那么就需要使用TDI来开发。对于XP以上的系统,Windows提供了一种新的WFP(Windows Filtering Platform)网络过滤层的框架来取代TDI。
TDI常常用来开发网络安全相关的产品,例如:
- 防火墙产品。
- 网络嗅探工具。
- 截包分析工具。
虽然Windows声明了TDI已经是舍弃的技术框架,但是经过实测TDI依旧能够在最新的系统上面稳定运行(Windows10),例如NetFilterSdk提供的TDI的SDK仍旧可以非常稳定运行,甚至比WFP表现得更佳。
为了使得我们开发的代码或者产品能够稳定运行在Windows的各个环境下面,TDI目前也是一种比较重要的技术,本文我们来分析一下TDI的开发技术。
1. 技术概述
首先我们来看一下Windows的网络模块架构,如下:
在这个框架中:
- ws2_32.dll提供了基本的socket相关函数(例如
socket
,bind
,listen
等)。 - Windows在用户层提供了一种过滤网络数据包的HOOK方案,这个就是Layered service provider(也就是我们通常说的LSP),通过这种技术我们对网络包进行HOOK了,国内很多大厂用的都是这种技术。
- Socket是一种统一的规范,无论是Windows还是Linux他们对外提供的接口都是一样的。在Windows下面Socket被转换成为设备的IO操作,并且提供了一个AFD.SYS(Ancillary Function Driver for WinSock)的驱动模块来辅助。
- tcpip是一个网络协议驱动程序,对底层他提供了一个NIDS协议驱动,对上层他提供了应对TCP,UDP,RAWIP等不同协议的设备对象。
TDI驱动的核心就是对于\\Device\\Tcp
,\\Device\\Udp
以及\\Device\\RawIp
三个设备进行过滤,形成设备栈,然后对每个IRP进行处理。
2. 设备的挂载
对于网络设备的过滤,只需要挂载三个设备即可,使用IoAttachDevice
就可以挂载设备对象,实现如下:
NTSTATUS
AttachDevice(
PDRIVER_OBJECT DriverObject,
PDEVICE_OBJECT *FltObject,
PDEVICE_OBJECT *OldObj,
CONST WCHAR*NetDevName)
{
NTSTATUS Status;
UNICODE_STRING DeviceName;
Status = IoCreateDevice(DriverObject,
0,
NULL,
FILE_DEVICE_UNKNOWN,
0,
TRUE,
FltObject);
if (Status != STATUS_SUCCESS)
{
return Status;
}
(*FltObject)->Flags |= DO_DIRECT_IO;
RtlInitUnicodeString(&str, DevName);
Status = IoAttachDevice(*FltObject, &str, OldObj);
if (Status != STATUS_SUCCESS)
{
return Status;
}
return STATUS_SUCCESS;
}
因此我们只需要针对\\Device\\Tcp
,\\Device\\Udp
以及\\Device\\RawIp
这三个对象调用AttachDevice
就可以对设备上面的网络数据进行过滤了。
3. TDI事件概述
当我们首先来看一下TDI涉及到的事件和IRP有哪些,如下:
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define TDI_ASSOCIATE_ADDRESS (0x01)
#define TDI_DISASSOCIATE_ADDRESS (0x02)
#define TDI_CONNECT (0x03)
#define TDI_LISTEN (0x04)
#define TDI_ACCEPT (0x05)
#define TDI_DISCONNECT (0x06)
#define TDI_SEND (0x07)
#define TDI_RECEIVE (0x08)
#define TDI_SEND_DATAGRAM (0x09)
#define TDI_RECEIVE_DATAGRAM (0x0A)
#define TDI_SET_EVENT_HANDLER (0x0B)
#define TDI_QUERY_INFORMATION (0x0C)
#define TDI_SET_INFORMATION (0x0D)
#define TDI_ACTION (0x0E)
#define TDI_EVENT_CONNECT ((USHORT)0) // TDI_IND_CONNECT event handler.
#define TDI_EVENT_DISCONNECT ((USHORT)1) // TDI_IND_DISCONNECT event handler.
#define TDI_EVENT_ERROR ((USHORT)2) // TDI_IND_ERROR event handler.
#define TDI_EVENT_RECEIVE ((USHORT)3) // TDI_IND_RECEIVE event handler.
#define TDI_EVENT_RECEIVE_DATAGRAM ((USHORT)4) // TDI_IND_RECEIVE_DATAGRAM event handler.
#define TDI_EVENT_RECEIVE_EXPEDITED ((USHORT)5) // TDI_IND_RECEIVE_EXPEDITED event handler.
#define TDI_EVENT_SEND_POSSIBLE ((USHORT)6) // TDI_IND_SEND_POSSIBLE event handler
这些事件大致可以分为三类:
- 基本的IRP操作,最主要的是用来打开IP,UDP等设备使用,以及传送TDI请求。
- TDI请求,这个主要是用户层SOCKET发起请求调用。
- TDI事件,这个一般是注册接收网络数据引发的回调函数。
对于TCP服务端,这些事件的交互过程可以描述为如下:
对于TCP客户端时主动发起Connect的一端,这些事件的交互过程可以描述为如下:
对于TDI的这些所有事件都是通过TidBuildXxx
函数来进行创建IRP的,如下:
Request | Build Function | AFD/TCPIP Usage |
---|---|---|
TDI_ASSOCIATE_ADDRESS | TdiBuildAssociateAddress() | Associate an address and connection object for a TCP socket. |
TDI_DISASSOCIATE_ADDRESS | TdiBuildDisassociateAddress() | Break the association between an address and a connection object that were previously associated with each other. |
TDI_CONNECT | TdiBuildConnect() | Initiate a TCP 3-way setup handshake (SYN, SYN+ACK, ACK) using a particular connection object. |
TDI_LISTEN | TdiBuildListen() | Solicit notifications for inbound SYNs on a particular address object. |
TDI_ACCEPT | TdiBuildAccept() | Request the TCP stack to respond to an inbound SYN with a SYN+ACK using a particular connection object. |
TDI_DISCONNECT | TdiBuildDisconnect() | Initialize a TCP 3-way teardown (FIN, FIN+ACK, ACK) handshake with a remote system. |
TDI_SEND | TdiBuildSend() | Transmit stream data using the specified connection object. |
TDI_RECEIVE | TdiBuildReceive() | Request TCP to return data received on a specific connection object. |
TDI_SEND_DATAGRAM | TdiBuildSendDatagram() | Transmit datagram packet(s) using the specified address object. |
TDI_RECEIVE_DATAGRAM | TdiBuildReceiveDatagram() | Request UDP to return data received on a specific address object. |
TDI_SET_EVENT_HANDLER | TdiBuildSetEventHandler() | Register/Unregister event callbacks for a given address object with TCPIP.sys. |
TDI_QUERY_INFORMATION | TdiBuildQueryInformation() | Read Management Information Base (MIB) information from the TCP/UDP/IP stack. |
TDI_SET_INFORMATION | TdiBuildSetInformation() | Not Supported by TCPIP.sys. |
TDI_ACTION | TdiBuildAction() | Not Supported by TCPIP.sys. |
4. IRP_MJ_CREATE
IRP_MJ_CREATE
这个是TDI比较重要的一个点,TDI有两个对象需要创建:
- 地址对象:所谓地址对象就是通常我们调用bind函数绑定的本地地址和端口的对象。
- 连接对象:所谓连接对象就是我们listen或者connect创建的,用来和远端连接通信的一个对象。
对于IRP_MJ_CREATE
,地址或者连接信息在CreateFile
的EaBuffer
参数中指定;对于bind
和listen
或者connect
使用两种不同的方式:
//bind
EaInfo->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH;
RtlCopyMemory(EaInfo->EaName,
TdiTransportAddress,
TDI_TRANSPORT_ADDRESS_LENGTH);
EaInfo->EaValueLength = sizeof(TA_IP_ADDRESS);
Address =
(PTRANSPORT_ADDRESS)(EaInfo->EaName + TDI_TRANSPORT_ADDRESS_LENGTH + 1);
TaCopyTransportAddressInPlace(Address, Name);
//connect和listen
EaInfo->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH;
RtlCopyMemory(EaInfo->EaName,
TdiConnectionContext,
TDI_CONNECTION_CONTEXT_LENGTH);
EaInfo->EaValueLength = sizeof(PVOID);
ContextArea = (PVOID*)(EaInfo->EaName + TDI_CONNECTION_CONTEXT_LENGTH + 1);
*ContextArea = NULL;
因此我们通过通过如下两个字符串来判断当前创建的是地址对象还是连接对象:
#define TdiTransportAddress "TransportAddress"
#define TdiConnectionContext "ConnectionContext"
#define TDI_TRANSPORT_ADDRESS_LENGTH (sizeof (TdiTransportAddress) - 1)
#define TDI_CONNECTION_CONTEXT_LENGTH (sizeof (TdiConnectionContext) - 1)
一般来说,对于我们TDI来说一个非常重要的操作就是获取地址;因为bind的时候我们有时候需要让系统选择一个可以使用的地址和端口,因此我们在IRP_MJ_CREATE
过滤的时候并不能获取到绑定的地址,需要使用如下方法:
- 设置IRP的完成例程。
- 在完成例程中使用
TdiBuildInternalDeviceControlIrp
创建一个TDI_QUERY_INFORMATION
的IRP。 - 使用
TdiBuildQueryInformation
设置查询类型为TDI_QUERY_ADDRESS_INFO
,并设置完成例程。 - 调用
IoCallDriver
发起IRP查询请求。 - 这样我们在
TDI_QUERY_ADDRESS_INFO
的完成例程中就可以获取到绑定的地址了。
在TdiTransportAddress
类型中通过TdiBuildInternalDeviceControlIrp
和TdiBuildQueryInformation
函数获取到绑定的地址信息的声明如下:
PIRP TdiBuildInternalDeviceControlIrp(
[in] CCHAR IrpSubFunction, // TDI_QUERY_INFORMATION
[in] PDEVICE_OBJECT DeviceObject,
[in] PFILE_OBJECT FileObject,
[in] PKEVENT Event,
[in] PIO_STATUS_BLOCK IoStatusBlock
);
VOID TdiBuildQueryInformation(
[in] PIRP Irp,
[in] PDEVICE_OBJECT DevObj,
[in] PFILE_OBJECT FileObj,
[in] PVOID CompRoutine,
[in] PVOID Contxt,
[in] UINT QType, //TDI_QUERY_ADDRESS_INFO
[in] PMDL MdlAddr
);
获取到的IP地址信息如下:
typedef struct _TDI_ADDRESS_INFO {
ULONG ActivityCount;
TRANSPORT_ADDRESS Address;
} TDI_ADDRESS_INFO, *PTDI_ADDRESS_INFO;
typedef struct _TRANSPORT_ADDRESS {
LONG TAAddressCount;
TA_ADDRESS Address[1];
} TRANSPORT_ADDRESS, *PTRANSPORT_ADDRESS;
typedef struct _TA_ADDRESS {
USHORT AddressLength;
USHORT AddressType;
UCHAR Address[1]; //Contains the TDI_ADDRESS_IP structure
} TA_ADDRESS, *PTA_ADDRESS;
typedef struct _TDI_ADDRESS_IP {
USHORT sin_port;
ULONG in_addr;
UCHAR sin_zero[8];
} TDI_ADDRESS_IP, *PTDI_ADDRESS_IP;
5. TDI_ASSOCIATE_ADDRESS
当一个连接对象被创建直接,就需要和本地的地址对象进行绑定,这个绑定的消息就是TDI_ASSOCIATE_ADDRESS
。
因此一般当我们接收到IRP_MJ_CREATE
创建连接对象的消息之后,接下来就会接收到TDI_ASSOCIATE_ADDRESS
地址绑定的消息,一般我们IrpSp->Parameters
中就是我们需要绑定的地址对象,如下:
//IrpSp->Parameters结构
struct _TDI_REQUEST_KERNEL_ASSOCIATE {
HANDLE AddressHandle;
} TDI_REQUEST_KERNEL_ASSOCIATE, *PTDI_REQUEST_KERNEL_ASSOCIATE;
6. TDI_SET_EVENT_HANDLER
TDI_SET_EVENT_HANDLER
这个是设置事件处理函数的请求,当下层到来网络操作时,会直接调用TDI_SET_EVENT_HANDLER
设置的函数,通过函数参数来传递信息,就不需要创建irp了,那么自然避免了irp的效率损失。
TDI_SET_EVENT_HANDLER
可以设置如下事件类型的请求:
#define TDI_EVENT_CONNECT 0
#define TDI_EVENT_DISCONNECT 1
#define TDI_EVENT_ERROR 2
#define TDI_EVENT_RECEIVE 3
#define TDI_EVENT_RECEIVE_DATAGRAM 4
#define TDI_EVENT_RECEIVE_EXPEDITED 5
#define TDI_EVENT_SEND_POSSIBLE 6
#define TDI_EVENT_CHAINED_RECEIVE 7
#define TDI_EVENT_CHAINED_RECEIVE_DATAGRAM 8
#define TDI_EVENT_CHAINED_RECEIVE_EXPEDITED 9
#define TDI_EVENT_ERROR_EX 10
回调事件是为了优化内存使用,传输时候用底层网络驱动直接传输数据缓冲,整理如下:
Event | AFD/TCP Usage |
---|---|
TDI_EVENT_CONNECT | AFD.sys指示到来的TCP SYN报文段 |
TDI_EVENT_DISCONNECT | AFD.sys指示到来的TCP FIN报文段 |
TDI_EVENT_ERROR | 没被TCPIP.sys使用 |
TDI_EVENT_RECEIVE | AFD.sys指示到来的TCP 数据报文段 |
TDI_EVENT_RECEIVE_DATAGRAM | AFD.sys指示到来的UDP数据报文段 |
TDI_EVENT_RECEIVE_EXPEDITED | AFD.sys指示带有TCP URGENT标记的数据报文段 |
TDI_EVENT_SEND_POSSIBLE | 没被TCPIP.sys使用 |
TDI_EVENT_CHAINED_RECEIVE | AFD.sys用NDIS_BUFFER描述的数据,不使用TCPIP的缓冲区且对该数据进行复制 |
TDI_EVENT_CHAINED_RECEIVE_DATAGRAM | 没被TCPIP.sys使用 |
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED | 没被TCPIP.sys使用 |
TDI_EVENT_ERROR_EX | TCPIP.sys向AFD.sys通知,远端不可达错误 |
一般来说我们可以使用TdiBuildSetEventHandler
创建TDI_SET_EVENT_HANDLER
的IRP,并设置回调函数,然后通过IoCallDriver
进行发送。
VOID TdiBuildSetEventHandler(
[in] PIRP Irp,
[in] PDEVICE_OBJECT DevObj,
[in] PFILE_OBJECT FileObj,
[in] PVOID CompRoutine,
[in] PVOID Contxt,
[in] LONG InEventType,
[in] PVOID InEventHandler,
[in] PVOID InEventContext
);
例如我们对于TDI_EVENT_CONNECT
事件的处理函数如下:
NTSTATUS ClientEventConnect(
_In_ PVOID TdiEventContext,
_In_ LONG RemoteAddressLength,
_In_ PVOID RemoteAddress,
_In_ LONG UserDataLength,
_In_ PVOID UserData,
_In_ LONG OptionsLength,
_In_ PVOID Options,
_Out_ CONNECTION_CONTEXT *ConnectionContext,
_Out_ PIRP *AcceptIrp
);
//RemoteAddress结构
typedef struct _TRANSPORT_ADDRESS {
LONG TAAddressCount;
TA_ADDRESS Address[1];
} TRANSPORT_ADDRESS, *PTRANSPORT_ADDRESS;
当网络协议栈从地城接收到了Connect事件(例如SYN消息)的时候,就会导致TDI_EVENT_CONNECT
事件的处理函数被调用。
TDI各种不同的事件对应不同的函数,如果在项目中需要处理不同事件对应的数据包,应该仔细参考MSDN来实现不同的回调函数。
对于事件的过滤,我们只需要替换回调函数即可,实现如下:
pAddr->ev_connect = pEvent->EventHandler;
pAddr->ev_connect_context = pEvent->EventContext;
pEvent->EventHandler = (PVOID) tcp_TdiConnectEventHandler;
pEvent->EventContext = (PVOID) pAddr;
7. 其他事件和回调函数
对于其他TDI的消息和事件,都有对应的socket函数与之对应,下面简单看几个典型的消息和事件。
7.1 TDI_CONNECT
TDI_CONNECT
是用户层发起connect
,对于这个请求,参数信息如下:
//IrpSp->Parameters
typedef struct _TDI_REQUEST_KERNEL {
ULONG_PTR RequestFlags;
PTDI_CONNECTION_INFORMATION RequestConnectionInformation;
PTDI_CONNECTION_INFORMATION ReturnConnectionInformation;
PVOID RequestSpecific;
} TDI_REQUEST_KERNEL, *PTDI_REQUEST_KERNEL;
typedef struct _TDI_CONNECTION_INFORMATION {
LONG UserDataLength;
PVOID UserData;
LONG OptionsLength;
PVOID Options;
LONG RemoteAddressLength;
PVOID RemoteAddress;
} TDI_CONNECTION_INFORMATION, *PTDI_CONNECTION_INFORMATION;
从参数RequestConnectionInformation
我们可以获取到连接的远程地址。
7.2 TDI_SEND
当用户层调用send
发送数据包的时候,TDI驱动就会接收到TDI_SEND
类型的事件,对于这个请求有两个参数:
IrpSp->Parameters
表示发送数据的信息,为TDI_REQUEST_KERNEL_SEND
结构。Irp->MdlAddress
表示发送数据的具体内容。
TDI_REQUEST_KERNEL_SEND
定义如下:
struct _TDI_REQUEST_KERNEL_SEND {
ULONG SendLength;
ULONG SendFlags;
} TDI_REQUEST_KERNEL_SEND, *PTDI_REQUEST_KERNEL_SEND;
对于SendFlags
可以取如下值:
TDI_SEND_EXPEDITED
:紧急包,必需放到传输层的头部。TDI_SEND_PARTIAL
:只是一部分数据,接下来还会发送数据。
7.3 TDI_EVENT_RECEIVE
对于TDI_EVENT_RECEIVE
事件,表示接收到数据(recv的被动),响应函数如下:
NTSTATUS ClientEventReceive(
_In_ PVOID TdiEventContext,
_In_ CONNECTION_CONTEXT ConnectionContext,
_In_ ULONG ReceiveFlags,
_In_ ULONG BytesIndicated,
_In_ ULONG BytesAvailable,
_Out_ ULONG *BytesTaken,
_In_ PVOID Tsdu,
_Out_ PIRP *IoRequestPacket
);
在这个函数中:
Tsdu
表示接收到的数据。- 返回
STATUS_SUCCESS
表示Tsdu
中的数据全部拷贝完毕(通过BytesAvailable
和BytesTaken
来指示)。 - 如果返回
STATUS_MORE_PROCESSING_REQUIRED
表示通过IoRequestPacket
来重新读取剩余的Tsdu
中的数据(一般是先读取BytesIndicated
长度数据到内部缓存)。 - 返回
STATUS_DATA_NOT_ACCEPTED
表示拒绝接收数据。
对于TDI_EVENT_RECEIVE
是在底层接收完成之后,分发数据包的时候调用的回调函数.
8. TDI实现流程
通过上面,我们基本可以大致了解了TDI的基本技术点,现在我们看一下TDI驱动的基本实现流程:
- 首先对TCP,UDP,RawIP等设备进行挂载和过滤。
- 然后设置各种回调函数,主要是
IRP_MJ_CREATE
,IRP_MJ_CLEANUP/IRP_MJ_CLOSE
,IRP_MJ_INTERNAL_DEVICE_CONTROL
。 - 在
IRP_MJ_CREATE
记录创建的地址对象和连接对象,并将其记录在哈希表中。 - 对
IRP_MJ_INTERNAL_DEVICE_CONTROL
中的各种TDI_XXX
进行处理。 - 对
TDI_SET_EVENT_HANDLER
各种回调函数进行处理。
例如我们可以对TDI_CONNECT
和TDI_EVENT_CONNECT
创建TCP的主动连接和被动连接的防火墙功能,也可以对TDI_SEND
和TDI_EVENT_RECEIVE
等消息实现网络数据嗅探功能。
9. 引用和参考
关于TDI我们可以参考如下技术链接:
- https://codemachine.com/articles/tdi_overview.html。
- https://sourceforge.net/projects/tdifw/。
- https://netfiltersdk.com/。
对于上述参考链接中NetFilter SDK 2是最全面的,但遗憾的是它并不是开源的。这个SDK提供了网络TDI的基本全部功能,如果有机会这个是我们学习比较全面的代码和资料。后面本人也将会陆续分享基于TDI的各种技术的应用开发示例。