TDI FILTER 網路過濾驅動完全解析
TDI FILTER 過濾驅動的功能一般用來進行整個系統中的所有網路流量的分析,記錄和管理,可以實現非常強大的管理功能,這裡就將討論它的設計架構,和具體實現的方法。
進行系統級網路資料包的過濾,很明顯,第一步需要在系統核心中擷取到網路資料包,那麼在WINDOWS平臺下,應該如何實現這樣的功能?
在WINDOWS核心中,資料的通訊載體是IRP包,如果希望擷取到IRP資料包,當然必須生成核模組以驅動的方式載入至核心之中。如果只是需要用來進行IRP資料包的擷取,進而進行資料的分析,及下一步工作的控制。比較合適的方式就是使用TDI FILTER驅動的方式。
它在核心中的結構如圖所示:
TDI FILTER ( 你的DRIVER )
TDI DRIVER ( AFD.SYS )
附加至TDI裝置的方法:
在DriverEntry時,生成兩個裝置,將其附加至(Attach)至Tdi驅動的Udp和Tcp裝置,實現IRP包過濾功能,具體程式碼如下:
#define UDP_DEVICE_NAME L"//Device//Udp
#define TCP_DEVICE_NAME L"//Device//Tcp"
#define TDI_FILTER_DEVICE_NAME L"//Device//TdiFilter"
typedef struct __TDI_FILTER_DEVICE_EXTENSION
{
PDEVICE_OBJECT pTdiDeviceObject; //過濾裝置至少需要記錄下真正Tdi網路裝置的指標,來呼叫真正的TDI裝置功能。
} TDI_FILTER_DEVICE_EXTENSION, *PTDI_FILTER_DEVICE_EXTENSION;
DriverEntry( PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath )
{
UNICODE_STRING TdiDeviceName;
......
RtlInitUnicodeString( ( PUNICODE_STRING )&TdiDeviceName,
UDP_DEVICE_NAME );
ntStatus = IoCreateDevice(
DriverObject,
sizeof( TDI_FILTER_DEVICE_EXTENSION ), //指定裝置擴充套件長度
NULL,
FILE_DEVICE_NETWORK, //網路型別裝置
0,
0,
&DeviceObject
); //生成一個無名、網路型別裝置,附加至TDI TCP/UDP裝置,實現過濾功能。
if( NT_SUCCESS( ntStatus ) )
{
DeviceObject->Flags |= DO_DIRECT_IO; //生成新的頁表將同樣的使用者記憶體空間對映至系統虛擬記憶體空間來進行通訊
ntStatus = IoAttachDevice(
DeviceObject,
TdiDeviceName,
( PDEVICE_OBJECT* )DeviceObject->DeviceExtension //附加至的裝置的指標將會輸出至此引數中,這樣就將真正的TDI裝置的指標記錄在過濾裝置的擴充套件中
);
}
}
TDI驅動的組織結構分為兩個部分:
1.龐大的INTERNAL IO CONTROL子功能,包括以下功能:
TDI_ASSOCIATE_ADDRESS 可以通過它截取出自己和對端的套接字資訊,一般就是IP地址+埠號,可以在此IRP功能響應中進行套接字資訊的記錄。
TDI_DISASSOCIATE_ADDRESS 它的IRP包是在closesocket函式時發生的,所以如果我們在TDI_ASSOCIATE_ADDRESS中記錄了資訊,需要在此IRP的功能響應中取消之前的記錄。
TDI_CONNECT
TDI_LISTEN
TDI_ACCEPT
TDI_DISCONNECT
TDI_SEND 它的IRP包是在呼叫send函式時發生的,必然,對它的響應將會實現對基於TCP協議的網路上傳流量的擷取。
TDI_RECEIVE 它的IRP包是在呼叫recv函式時發生的,必然,對它的響應將會實現對基於TCP協議的網路下載流量的擷取。
TDI_SEND_DATAGRAM 它的IRP包是在呼叫sendto函式時發生的,必然,對它的響應將會實現對基於UDP協議的網路上傳流量的擷取。
TDI_RECEIVE_DATAGRAM 它的IRP包是在呼叫recvfrom函式時發生的,必然,對它的響應將會實現對基於UDP協議的網路下載流量的擷取。
TDI_SET_EVENT_HANDLER 它的IRP包是在TDI驅動中註冊一些回撥用函式,當接收到資料包時,將會首先執行它們,它的具體功能將會在下一步講述。
TDI_QUERY_INFORMATION
TDI_SET_INFORMATION
TDI_ACTION
TDI_DIRECT_SEND
TDI_DIRECT_SEND_DATAGRAM
在TDI_SET_EVENT_HANDLER子功能中,可以註冊以下回調涵數:
TDI_EVENT_CONNECT
TDI_EVENT_DISCONNECT
TDI_EVENT_ERROR
TDI_EVENT_RECEIVE 對應於recv函式有返回資料時,將會呼叫此回撥函式。
TDI_EVENT_RECEIVE_DATAGRAM 對應於recvfrom函式接收到資料時,將會呼叫此回撥函式。
TDI_EVENT_RECEIVE_EXPEDITED 對應於函式接收到帶外資料時,將會呼叫此回撥函式。( 帶外數也就是OOB資料, 在全部IRP資料包中會優先進行傳送或接收,TCP協議功能 )
TDI_EVENT_SEND_POSSIBLE
以下將講述資料具體傳輸回撥功能的過濾方法
4.實現事件回撥函式掛鉤的方法:
響應IRP_MJ_INTERNAL_DEVICE_CONTROL中的TDI_SET_EVENT_HANDLER子功能,記錄下原始的註冊事件回撥函式和引數,但真正註冊的是自己的回撥函式,來擷取所有的事件回撥函式呼叫,實現過濾功能。
具體程式碼如下:
typedef struct __TDI_EVENT_CONTEXT_WRAP
{
DWORD dwEventContextMark; //對自己生成的結構例項加一個四位元組的標誌,可以不使用。
DWORD dwEventType; //記錄事件回撥函式的型別
PVOID pOrgEventHandler; //記錄原始的事件回撥函式
PVOID pOrgEventContext; //記錄原始的事件回撥函式引數
PFILE_OBJECT pAssocAddr; //記錄事件回撥函式所繫結的本機套接字
PDEVICE_OBJEXT pDeviceObjext; //記錄註冊事件IRP所傳送至的TDI裝置
} TDI_EVENT_HANDLER_WRAP, *PTDI_EVENT_HANDLER_WRAP;
typedef struct __TDI_EVENT_HANDLER_LINK
{
LIST_ENTRY List; //將事件回撥鉤子記錄以連結串列形式進行管理
PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
} TDI_EVENT_HANDLER_LIST, *PTDI_EVENT_HANDLER_LIST;
LIST_ENTRY g_TdiEventHandlerInfoList;
NTSTATUS DeviceInternalIoControl( PDEVICE_OBJECT DeviceObject, PIRP Irp )
{
PKIRQL OldIrql;
PLIST_ENTRY pListEntry;
PIO_STACK_LOCATION IrpSp;
PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList;
PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap_;
PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList_;
PTDI_FILTER_DEVICE_EXTENSION pTdiDeviceExtension;
pTdiDeviceExtension = ( PTDI_FILTER_DEVICE_EXTENSION )DeviceObject->DeviceExtension;
switch( IrpSp->MinorFunction )
{
case TDI_SET_EVENT_HANDLER:
pTdiSetEvent = ( PTDI_REQUEST_KERNEL_SET_EVENT )&pIrpSp->Parameters;
if( TDI_EVENT_RECEIVE == pTdiSetEvent->EventType )//||
TDI_EVENT_RECEIVE_EXPEDITED == pTdiSetEvent->EventType ||
TDI_EVENT_CHAINED_RECEIVE == pTdiSetEvent->EventType ||
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED == pTdiSetEvent->EventType ||
TDI_EVENT_RECEIVE_DATAGRAM == pTdiSetEvent->EventType )
{
pTdiEventHandlerList = NULL;
pTdiEventHandlerWrap = NULL;
pProcessNetWorkTrafficInfo = NULL;
if( NULL == pTdiSetEvent->EventHandler )
{
//注意!如果註冊的事件回撥函式是NULL的話,它表示的取消之前曾經註冊過的事件回撥函式, 這裡當然不能掛鉤,可以加入釋放鉤子資源的操作。
goto CALL_PDO_DRIVER;
}
KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql ); //對事件回撥函式鉤子列表寫操作加鎖保護
pListEntry = g_TdiEventHandlerInfoList.Flink;
for( ; ; )
{
if( pListEntry == &g_TdiEventHandlerInfoList )
{
pTdiEventHandlerWrap_ = NULL;
break;
}
pTdiEventHandlerList_ = ( PTDI_EVENT_HANDLER_LIST )pListEntry;
pTdiEventHandlerWrap_ = pTdiEventHandlerList_->pTdiEventHandlerWrap;
if( pTdiEventHandlerWrap_->pAssocAddr == pFileObject &&
pTdiEventHandlerWrap_->dwEventType == dwEventType ) //如果此本機套接字物件的相應事件回撥函式已經存在,則直接對其進行修改就可以了,而不是不斷的新建事件件回撥鉤子
{
pTdiEventHandlerWrap_->pOrgEventHandler = pEventHandler;
pTdiEventHandlerWrap_->pOrgEventContext = pEventContext;
break;
}
}
if( NULL == pTdiEventHandlerWrap_ ) //沒有找到,加入新的事件回撥函式鉤子
{
pTdiEventHandlerWrap = ( PTDI_EVENT_HANDLER_WRAP )ExAllocatePoolWithTag( NonPagedPool, sizeof( TDI_EVENT_HANDLER_WRAP ), 0 );
if( NULL == pTdiEventHandlerWrap )
{
goto RELEASE_RESOURCE;
}
pTdiEventHandlerList = ( PTDI_EVENT_HANDLER_LIST )ExAllocatePoolWithTag( NonPagedPool, sizeof( TDI_EVENT_HANDLER_LIST ), 0 );
if( NULL == pTdiEventHandlerList )
{
goto RELEASE_RESOURCE;
}
pTdiEventHandlerWrap->dwEventContextMark = TDI_EVENT_CONTEXT_MARK;
pTdiEventHandlerWrap->dwEventType = dwEventType;
pTdiEventHandlerWrap->pOrgEventHandler = pEventHandler;
pTdiEventHandlerWrap->pOrgEventContext = pEventContext;
pTdiEventHandlerWrap->pAssocAddr = pFileObject;
pTdiEventHandlerWrap->pDeviceObject = pTdiDeviceExtension->pTdiDeviceObject;
pTdiEventHandlerList->pTdiEventHandlerWrap = pTdiEventHandlerWrap;
InsertTailList( &g_TdiEventHandlerInfoList, pTdiEventHandlerList );
}
else
{
pTdiEventHandlerWrap = pTdiEventHandlerWrap_;
pTdiEventHandlerList = pTdiEventHandlerList_;
}
KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql ); //釋放事件回撥鉤子列表鎖
if( TDI_EVENT_RECEIVE == pTdiSetEvent->EventType ||
TDI_EVENT_RECEIVE_EXPEDITED == pTdiSetEvent->EventType )
{
pTdiSetEvent->EventHandler = TdiFilterRecvEventHandler; //加入自己的事件過濾回撥函式
}
else if( TDI_EVENT_CHAINED_RECEIVE == pTdiSetEvent->EventType ||
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED == pTdiSetEvent->EventType )
{
pTdiSetEvent->EventHandler = TdiFilterChainedRecvHandler;
}
else
{
pTdiSetEvent->EventHandler = TdiFilterRecvDatagramEventHandler;
}
pTdiSetEvent->EventContext = pTdiEventHandlerWrap;
IoSkipCurrentIrpStackLocation( pIrp );
ntStatus = IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
if( !NT_SUCCESS( ntStatus ) )
{
if( NULL == pTdiEventHandlerWrap_ )
{
//如果是新加入的事件回撥函式鉤子,可以在出錯時將其釋放, 也可以保留至套接字關閉時,再進行釋放
KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql ); //對事件回撥函式鉤子列表寫操作加鎖保護
RemoveEntryList( ( PLIST_ENTRY )pTdiEventHandlerList );
ExFreePoolWithTag( pTdiEventHandlerWrap );
ExFreePoolWithTag( pTdiEventHandlerList );
KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql ); //釋放事件回撥鉤子列表鎖
}
}
return ntStatus;
}
break;
default:
goto CALL_PDO_DRIVER;
break;
}
RELEASE_RESOURCE:
if( NULL != pTdiEventHandlerWrap )
{
ExFreePoolWithTag( pTdiEventHandlerWrap, NonPagedPool );
}
if( NULL != pTdiEventHandlerList )
{
ExFreePoolWithTag( pTdiEventHandlerList, NonPagedPool );
}
CALL_PDO_DRIVER:
IoSkipCurrentIrpStackLocation( pIrp );
return IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
}
以上對事件回撥函式加入了鉤子,下一步,必須考慮對其釋放的問題,否則,當原始回撥函式對應的套接字釋放後,你的系統將會崩潰,以下為具體程式碼:
在套接字關閉後,要在IRP_MJ_CLEANUP功能函式中將相關的事件回撥鉤子釋放掉:
NTSTATUS TdiFilterCleanUp(PDEVICE_OBJECT DeviceObject, PIRP pIrp )
{
NTSTATUS ntStatus;
KIRQL OldIrql;
PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList;
PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
PFILE_OBJECT pFileObject;
TDI_FILTER_DEVICE_EXTENSION *pDeviceExtension;
PIO_STACK_LOCATION pIrpSp;
pDeviceExtension = ( TDI_FILTER_DEVICE_EXTENSION* )DeviceObject->DeviceExtension;
pIrpSp = IoGetCurrentIrpStackLocation( pIrp );
pFileObject = pIrpSp->FileObject;
...
//如果是主控制裝置,要將呼叫IoCompleteIrp完成Irp, 如果是過濾裝置,呼叫PDO裝置驅動
IoSkipCurrentIrpStackLocation( pIrp );
ntStatus = IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
if( !NT_SUCCESS( ntStatus ) )
{
DebugPrintEx( CLEANUP_INFO,"netmon TdiFilterCleanUp IoCallDriver return ERROR/n" );
return ntStatus;
}
//下一步,釋放套接字對應的事件回撥鉤子
KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql );
FIND_LIST_AGAIN:
pListEntry = g_TdiEventHandlerInfoList.Flink;
for( ; ; )
{
if( pListEntry == &g_TdiEventHandlerInfoList )
{
break;
}
pTdiEventHandlerList = ( PTDI_EVENT_HANDLER_LIST )pListEntry;
pTdiEventHandlerWrap = pTdiEventHandlerList->pTdiEventHandlerWrap;
if( pTdiEventHandlerWrap->pAssocAddr == pFileObject )
{
RemoveEntryList( pListEntry );
ExFreePoolWithTag( pTdiEventHandlerWrap, 0 );
ExFreePoolWithTag( pTdiEventHandlerList, 0 );
goto FIND_LIST_AGAIN;
}
pListEntry = pListEntry->Flink;
}
KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql );
return ntStatus;
}
那麼,可以在事件回撥過濾鉤子函式對資料進行處理了
NTSTATUS TdiFilterRecvEventHandler( 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
)
{
NTSTATUS ntStatus;
PIO_STACK_LOCATION pIrpSp;
PTDI_EVENT_HANDLER_WRAP pEventHandlerWrap;
PTDI_COMPLETION_WRAP pCompletionWrap;
LARGE_INTEGER RecvedDataSize;
pEventHandlerWrap = ( PTDI_EVENT_HANDLER_WRAP )TdiEventContext;
if( FALSE == g_bFiltering ) //是否進行過濾
{
goto CALL_ORIGINAL_EVENT_HANDLER;
}
if( FALSE != bStopRecv )
{
ntStatus = STATUS_DATA_NOT_ACCEPTED;
goto RELEASE_PROCESS_IO_INFO_RETURN;
}
ntStatus = ( ( ClientEventReceive )pEventHandlerWrap->pOrgEventHandler )(
pEventHandlerWrap->pOrgEventContext,
ConnectionContext,
ReceiveFlags,
BytesIndicated,
BytesAvailable,
BytesTaken,
Tsdu,
IoRequestPacket
);
if( NULL != BytesTaken &&
0 != *BytesTaken )
{
//這裡對資料進行處理, 比如可以進行通訊資料量的統計
}
if( STATUS_MORE_PROCESSING_REQUIRED != ntStatus )
{
goto RELEASE_PROCESS_IO_INFO_RETURN;
}
if( NULL == *IoRequestPacket )
{
goto RELEASE_PROCESS_IO_INFO_RETURN;
}
//IoRequestPacket表示當前接收IRP中的資料如果並不完整, 並且認為接下來的資料是有價值,需要接收的話,那麼需要自己新建一個IRP包,將其指標傳入此引數中,並返回STATUS_MORE_PROCESSING_REQUIRED,通知IO管理不終止此IRP,TDI驅動將繼續接收接下來的資料。
//所以如果此IRP包存在,可以擷取它的資訊,具體方法下一步講述。
return ntStatus;
CALL_ORIGINAL_EVENT_HANDLER:
return ( ( ClientEventReceive )pEventHandlerWrap->pOrgEventHandler )( //直接呼叫原始的IRP鉤子函式,不進行處理
pEventHandlerWrap->pOrgEventContext,
ConnectionContext,
ReceiveFlags,
BytesIndicated,
BytesAvailable,
BytesTaken,
Tsdu,
IoRequestPacket
);
}
上面講述了使用事件回撥函式鉤子的方式進行通訊資料的擷取方法,下面講述直接IRP包資料傳輸方式,也就是以下4個子功能的擷取方法:
TDI_SEND
TDI_RECEIVE
TDI_SEND_DATAGRAM
TDI_RECEIVE_DATAGRAM
在IRP_MJ_INTERNAL_DEVICE_CONTROL函式中響應以上子功能時,確認引數DeviceObject為TDI過濾裝置,對所有擷取到的IRP加入自己的完成函式,在此IRP被完成時( 呼叫IoCompleteRequest )此完成函式被呼叫,取得IRP處理的返回結果,進行處理。具體資料的處理對應於TDI_SEND子功能可以在IoCallDriver之前得到,因為它是應用程式傳給你的,而TDI_RECEIVE子功能,應該在TDI事件回撥函式或Completion回撥函式中取得。
相關程式碼如下:
typedef struct __TDI_COMPLETION_WRAP
{
...//可以加入用來記錄/處理資料的成員, 比如通訊標誌, 流量統計等
PIO_COMPLETION_ROUTINE pCompletionRoutine;
LPVOID pContext;
} TDI_COMPLETION_WRAP, *PTDI_COMPLETION_WRAP;
加入自定義的CompletionRoutine的方法:
if( TDI_SEND == MinorFunction ||
TDI_SEND_DATAGRAM == MinorFunction ||
TDI_RECEIVE == MinorFunction ||
TDI_RECEIVE_DATAGRAM == MinorFunction )
{
if( TDI_RECEIVE == MinorFunction &&
TDI_RECEIVE_PEEK == ( ULONG )pIrpSp->Parameters.Others.Argument2 )
{
//TDI_RECEIVE_PEEK不會真正接收資料,可以不需要對其進行過濾。
goto SKIP_CURRENT_STACK_LOCATION;
}
pCompletionWrap = ( PTDI_COMPLETION_WRAP )ExAllocateFromNPagedLookasideList( &g_CompletionWrapList ); //可以使用連結串列或HASH等資料結構來管理所有的CompletionRoutine包裝資訊,這裡使用了NPAGED_LOOKASIDE_LIST,它的優勢在於系統中所有的NPAGED_LOOKASIDE_LIST資源的最大佔用量將會被記憶體管理器動態管理
if( NULL == pCompletionWrap )
{
goto SKIP_CURRENT_STACK_LOCATION;
}
//這裡可以設定CompletionRoutine的具體工作引數,比如具體操作的型別,原始的Completion函式等,在使用者層傳送至的IRP中是不會設定CompletionRoutine函式的,但其它驅動傳送至的IRP中可能會進行設定,如在Receive事件回撥函式中的IoRequestPacket引數
IoCopyCurrentIrpStackLocationToNext( pIrp ); //設定下一個裝置棧工作引數
IoSetCompletionRoutine( pIrp,
TdiFilterCompletion,
pCompletionWrap,
TRUE,
TRUE,
TRUE
);//這裡就為這個IRP加入自己的CompletionRoutine函式
goto CALL_PDO_DRIVER;
SKIP_CURRENT_STACK_LOCATION:
IoSkipCurrentIrpStackLocation( pIrp );
CALL_PDO_DRIVER:
return IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
}
具體的Completion函式的工作:
NTSTATUS TdiFilterCompletion( PDEVICE_OBJECT pDeviceObject, PIRP pIrp, LPVOID pContext )
{
NTSTATUS ntStatus;
PTDI_COMPLETION_WRAP pCompletionWrap;
LARGE_INTEGER TransferredDataSize;
PIRP pMasterIrp;
PIO_STACK_LOCATION pIrpSp;
ntStatus = pIrp->IoStatus.Status;
pCompletionWrap = ( PTDI_COMPLETION_WRAP )pContext;
if( NT_SUCCESS( ntStatus ) )
{
//可以在這裡對成功傳輸的資料進行處理
}
//這裡可以呼叫原始的Completion函式
RETURN_SUCCESS:
return ntStatus;
}
需要注意的是,如果為IRP包加入了CompletionRoutine之後,那麼在驅動解除安裝( Unload )之前,必須保證所有IRP已經執行過此Completion函式, 如果在驅動被從記憶體中解除安裝後才執行, 將會使系統崩潰。
處理方法為:
1.不實現DriverUnload函式,使驅動只有在系統關閉,底層裝置被解除安裝時,才能完成真正的解除安裝。這是的一般FILTER驅動的工作方式,
2.使用執行緒同步的方法保證Completion函式的執行,Windows XP或之後的系統也提供了一個API, SetCompletionRoutineEx來保證驅動在Completion函式完成前不被解除安裝。
至此,講述TDI過濾驅動組織框架,可以為它新增一些更加完善的功能。