一個簡單的tcp filter的例子
來源:華中白雲黃鶴BBS
作者:huyuguang
這兩天版面日漸蕭條,我只好硬着頭皮把我自己的一個尚在開發,非常不成熟
的東西拿出來了。
前面我曾經就網絡加密這個議題討論過,那個時候我提出的方法是imd,
9x下就是利用hook_device_service,用這些方法的一個好處就是,接收數據
比較低層,直接得到的就是mac數據,而且可以知道是哪塊網卡上來的,而且,
可以加密各種數據幀,ipx,ip什麼的都不在話下。因爲我所遇到的加密環境和要求,
幾乎都是要求對tcp加密,在這種情況下,做在imd上的話,顯然會降低ipx或者icmp
之類的非加密數據包的效率。因此以前我就考慮在tcp 上做一個filter,直接加密tcp
的數據。
正好在mark的站點上新出來一個小工具,叫做tdimon,工具很好,可以看到各個進程
的通訊狀態。可惜的是1.沒有源代碼,2.free版本不能得到數據內容。有這兩點限制,這個
工具就只能當作玩具。正好那個時候我比較閒,就花了點時間做了一個類似的工具。
關於FILTER,art baker的書裏說得很詳細,我做的時候基本上也就是根據這本書上來的,
做tcp的filter,和做別的設備的filter,沒什麼區別,但是不同是由於不知道tcp是如何
和別的設備(tdi clinet)通訊的,所以必須仔細看tdi的規範。
我先描述一下2000/nt下的tcp/ip協議的一些情況。2000/nt下,ip,tcp,udp是在
一個驅動程序裏實現的,叫做tcp.sys,這個驅動程序創建了3個設備,就是ip,tcp,udp。
首先描述一下driverentry。首先當然是iocreatedevice,用file_device_unknown,
因爲tcp設備就是用的這個參數。代碼如下:
RtlInitUnicodeString( &usDeviceName, FILTER_NAME );
status = IoCreateDevice( DriverObject,
sizeof(DEVICE_EXTENSION),
&usDeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&pDevObj );
然後就調用iogetdeviceobjectpointer來得到tcp設備的指針。
代碼如下:
RtlInitUnicodeString( &usTargetName, TARGET_NAME );
status = IoGetDeviceObjectPointer( &usTargetName,
FILE_ALL_ACCESS,
&pTargetFileObj,
&pTargetDevObj );
注意TARGET_NAME是大小寫區分的,我是用#define TARGET_NAME L"//Device//Tcp",
不能寫成#define TARGET_NAME L"//Device//tcp"。
然後我們就開始調用IoAttachDeviceToDevieStatck插入到tcp設備。
調用完成後,我們要讓我們的設備表現的和tcp一樣,於是把它的所有
特性都從tcpobj複製(pdevobj->xxx=ptcpobj->xxx)。
再掃描他tcpdriverobject的majorfunction,我們的driver必須都
支持。最後設置driverunload,因爲爲了方便調試,我們必須寫一個
unload。
前面已經講過driverentry了,經過driverentry,所有的原來應該發送到
tcp設備的irp現在都發送到我們的設備的處理函數了,如果什麼事情都不做,
那麼可以簡單的調用iocalldriver把這個irp發到tcp設備去讓它處理。
當然,我們是要做些事情的,於是代碼如下:
UCHAR MajorFunction,MinorFunction;
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( Irp );
MajorFunction = pIrpStack->MajorFunction;
MinorFunction = pIrpStack->MinorFunction;
file://DBGPRINT( ...)
ParseIrp(Irp);
IoCopyCurrentIrpStackLocationToNext( Irp );
IoSetCompletionRoutine( Irp,
CompletionRoutine,
NULL, // context
TRUE, // InvokeOnSuccess
TRUE, // InvokeOnError
TRUE ); // InvokeOnCancel
return IoCallDriver( pDevExt->TargetDevObj, Irp);
代碼很簡單,除了parseirp之外,其他都是例行公事。對於tcp設備的前期處理
就在這個函數裏。後期處理的函數可以放在completionroution裏。
好了,我們已經得到了發向tcp設備的所有irp了,這個時候我們的任務就是要了解
tcp設備到底是如何工作的.
我們的任務是要得到本機發送出去的tcp數據和接受的tcp數據。當然前提是發送數據
通過tcp設備,如果某些應用程序用packet.sys之類的協議driver,直接發送tcp數據,
這種情況下數據不經過tcp設備,filter也就無從得到了。
爲了瞭解如何從發向tcp設備的irp中得到信息,首先我先描述一下tdi client是如何
與tcp通訊以及tdi client一般是如何工作的。
driverstdio裏面有好幾個例子都是關於tdi client的,但是這些例子都是基於它自己
的類庫的,不過這些例子功能都很強大,其中一個usb+web的溫度計的創意簡直讓我目瞪口呆。
因爲我也做過pci溫度計的driver,但是我從來就沒有想到還可以加上一個web server在裏面。
因爲driverstdio裏的例子太複雜,我在這裏簡單描述一下,給一個小例子。
因爲發送過程較爲簡單,先描述發送。
首先調用pIrp = TdiBuildInternalDeviceControlIrp (
TDI_SEND_DATAGRAM, // sub function
pDeviceObject, // pointer to device object
pTransportObject, // pointer to udp object
NULL, // pointer to event
NULL ); // pointer to return buffer
分配一塊irp,然後調用
TdiBuildSendDatagram (
pIrp, // pointer to irp
pDeviceObject, // pointer to device object
pTransportObject, // pointer to file object
NULL, // completion routine
NULL, // completion context
pMdl, // pointer to data
dBufferSize, // size of data
pConnectInfo ); // connection information
不用我說也應該知道,數據是放在一個buf裏面,調用這個函數之前,要先構件一個mdl,把這個buf
放進去。
然後...irp已經好了,只要IoCallDriver(pUDPObject,PIrp)就行了...
上面用的是UDP的例子(Datagram),但是tcp也是一樣,雖然函數有點差別,但是也是大同小異(TdiBuildSend)。
爲了搞清楚上面的兩個函數到底做了些什麼(到底構建的irp是什麼樣子),沒有必要去跟蹤,實際上,tdibuildxxx
都不過是一個宏,你可以在tdikrnl.h裏找到。打開tdikrnl.h看看,發現每個宏裏都有這麼一句:
_IRPSP->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;
於是我們知道tdi client就是通過majorfunction=IRP_MJ_INTERNAL_DEVICE_CONTROL,minorfunction=send/recv/...
和tcp通訊的。這就使得我們確信,這種方法是可行的。(不是直接調用tdi函數,而是發irp)
接收的過程較爲複雜(tdi client的選擇較多,因此要考慮各種情況)
開始的時候,我在ddk document裏看到這麼一個宏:tdibuildreceive,我想ok,
和send一樣,我當時想的很簡單,以爲tdi client要接受數據了,它就向tcp發一個
irp,然後返回pending,當有數據來的時候,tcp處理完這個irp,然後tdi client只要在這個irp的
irp_complete裏處理得到的數據就行了。這的確是tdi client的一個選擇,但是
當我試驗的時候,我發現至少wsock不是這樣做的,因爲我打開了一個ie,瀏覽了一個網頁,
但是我發現我發出的數據都很好的捕獲到了,但是接收的數據一個也沒有,而且事實上,
tcp似乎就沒有受到minorfunction=tdi_receive的irp。我因爲這件事苦惱了一段時間,
後來我仔細的讀了讀ddk document,發現tdi client還有第2種選擇,首先向tcp發送一個
irp,minorfunction=tdi_sethandler,設置一些回調函數,然後當事情發生的時候,tcp
就會調用這些回調函數,這些函數名字是clientEventxxx。這個過程可以仔細看ddk document,
看TdiBuildSetEventHandler,就知道哪些過程可以用這個方法。receive也是其中的一個,於是
我們的方法得到了。首先我們得到了irp,如果是set_eventhandler,那麼就修改tdi client
向tcp設置的回調函數的入口,把它指向我們自己寫的一個函數,同時保留原來的入口,
然後在我們自己寫的函數裏裏調用它。
代碼如下:
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( Irp );
MajorFunction = pIrpStack->MajorFunction;
MinorFunction = pIrpStack->MinorFunction;
FileObject = pIrpStack->FileObject ;
....
switch(MajorFunction)
{
...
case IRP_MJ_INTERNAL_DEVICE_CONTROL :
{
switch(MinorFunction)
{
...
case TDI_SET_EVENT_HANDLER:
pRequestSetEvent = (PTDI_REQUEST_KERNEL_SET_EVENT)(&(pIrpStack-> Parameters.DeviceIoControl)) ;
EventType=pRequestSetEvent->EventType;
EventHandler=pRequestSetEvent->EventHandler;
EventContext=pRequestSetEvent->EventContext;
...
switch(EventType)
{
...
case TDI_EVENT_RECEIVE :
pRequestSetEvent->EventHandler = OurClientEventReceive;
g_EventReceive = EventHandler;
break;
...
值得注意的是,就算是set_eventhandler,也不一定就是tdi_event_receive裏接受數據,
這個tdi_client的選擇很多,還有ClientEventChainedReceive 什麼的,詳情可看ddk,
winsock似乎都是沒有用過這個。不過這個不影響,我們可以把ddk裏面所有可能的情況
全部列出來,一一判斷,然後分別處理。對於clienteventreceive,這裏還有幾句話要說
清楚,並不是每次調用這個函數就得到了數據,這還要根據ReceiveFlags參數,tdi client
要根據這個參數決定是否要發tdi_receive irp得到數據。於是我們的處理函數就要判斷這個
參數,以便做出相應的處理,我自己的代碼在實際的環境中運行了一段時間,似乎沒有問題,但是
我也不敢說,對於各種情況我都考慮到了。