開發Windows 2000/XP下的防火牆
作者:Jesús O
翻譯:PowerCPP
下載本文配套原始碼
介紹
如果你決定開發LINUX下的防火牆,你會找到很多免費的資訊與原始碼。但如果開發WINDOWS平台下的防火牆會有點困難,找到相關資訊與代碼都簡直是不可能的任務。
因此我決定寫這篇文章介紹在WINDOWS 2000/XP下開發防火牆的簡單方法。
背景
在WINDOWS 2000 DDK中,微軟包含了稱為Filter-Hook Driver的新型網路驅動。你可以使用它來過濾所有進出介面的資料。
因為關於此的文檔很少並沒有代碼,我把使用它的成功方法寫入文章,希望協助你理解這種簡單的方法。
Filter-Hook 驅動
像我剛才所說的,在Microsoft Windows 2000 DDK中介紹了Filter-Hook Driver, 事實上,它不是一種新的網路驅動,它只是擴充了IP過濾驅動(IP Filter Driver)的功能。
實際上,Filter-Hook Driver並不是網路驅動,它是一種核心模式驅動(Kernel Mode Driver). 大致上是這樣的:在Filter-Hook Driver中我們提供回呼函數(callback),然後使用IP Filter Driver註冊回呼函數。這樣當資料包發送和接收時,IP Filter Driver會調用回呼函數。那麼我們到底該如何?這些步驟呢?總結如下:
1) 建立Filter-Hook Driver.我們必須建立核心模式驅動,你可以選擇名稱,DOS名稱和其它驅動特性,這些不是必須的,但我建議使用描述名稱。
2) 如果我們要安裝過濾函數,首先我們必須得到指向IP Filter Driver的指標,這是第二步。
3) 我們已經取得了指標,現在我們可以通過發送特殊的IRP來安裝過濾函數,該"訊息"傳遞的資料包含了過濾函數的指標。
4) 過濾資料包!!!
5) 當我們想結束過濾,我們必須撤銷過濾函數。這通過傳遞null指標作為過濾函數指標來實現。
哦,只有五個步驟,這看起來非常容易,但...如何產生核心模式驅動?如何得到IP Filter Driver指標,如何..... 是的,請稍等,我現在將解釋這些步驟並提供原始碼:P
建立核心模式驅動(Kernel Mode Driver)
Filter-Hook Driver屬於核心模式驅動,因此我們要建立核心模式驅動。這篇文章不是“如何僅用5分鐘開發核心模式驅動” 這樣的指南,因此我假設讀者已經有了關於此的相關知識。
Filter-Hook Driver結構是典型的核心模式驅動的結構:
1) 一個建立裝置的驅動程式入口,為通訊建立符號串連和處理IRPs(指派,載入,卸載,建立...)的標準常式。
2)在標準常式裡管理IRPs.在開始編碼前,我建議先思考一下哪些IOCTL你需要從裝置驅動中暴露給應用程式。 在我的例子中,我實現了四個IOCTL代碼:START_IP_HOOK(註冊過濾函數),STOP_IP_HOOK(登出過濾函數), ADD_FILTER(安裝新的過濾規則),CLEAR_FILTER(清除所有規則).
3)對於我們的驅動,我們必須實現多個用於過濾的函數。
我推薦你使用工具程式來產生核心驅動基本架構,這樣你只需往裡添加代碼。例如,我在工程中使用的是QuickSYS。
下面是驅動結構的實現代碼:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath){ //.... dprintf("DrvFltIp.SYS: entering DriverEntry/n"); //我們必須建立裝置 RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME); ntStatus = IoCreateDevice(DriverObject, 0, &deviceNameUnicodeString, FILE_DEVICE_DRVFLTIP, 0, FALSE, &deviceObject); if ( NT_SUCCESS(ntStatus) ) { // 建立符號串連使win32應用程式可以處理驅動與裝置 RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME); ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString, &deviceNameUnicodeString); //.... // 建立用於控制、建立、關閉的dispatch指標 DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DrvDispatch; DriverObject->DriverUnload = DrvUnload; } if ( !NT_SUCCESS(ntStatus) ) { dprintf("Error in initialization. Unloading..."); DrvUnload(DriverObject); } return ntStatus;}NTSTATUS DrvDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp){ // .... switch (irpStack->MajorFunction) { case IRP_MJ_CREATE: dprintf("DrvFltIp.SYS: IRP_MJ_CREATE/n"); break; case IRP_MJ_CLOSE: dprintf("DrvFltIp.SYS: IRP_MJ_CLOSE/n"); break; case IRP_MJ_DEVICE_CONTROL: dprintf("DrvFltIp.SYS: IRP_MJ_DEVICE_CONTROL/n"); ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode; switch (ioControlCode) { // 啟動過濾的ioctl代碼 case START_IP_HOOK: { SetFilterFunction(cbFilterFunction); break; } // 關閉過濾的ioctl case STOP_IP_HOOK: { SetFilterFunction(NULL); break; } // 添加過濾規則的ioctl case ADD_FILTER: { if(inputBufferLength == sizeof(IPFilter)) { IPFilter *nf; nf = (IPFilter *)ioBuffer; AddFilterToList(nf); } break; } // 釋放過濾規則列表的ioctl case CLEAR_FILTER: { ClearFilterList(); break; } default: Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; dprintf("DrvFltIp.SYS: unknown IRP_MJ_DEVICE_CONTROL/n"); break; } break; } ntStatus = Irp->IoStatus.Status; IoCompleteRequest(Irp, IO_NO_INCREMENT); // 我們不會有未決的操作,所以總是返回狀態代碼 return ntStatus;}VOID DrvUnload(IN PDRIVER_OBJECT DriverObject){ UNICODE_STRING deviceLinkUnicodeString; dprintf("DrvFltIp.SYS: Unloading/n"); SetFilterFunction(NULL); // 釋放所有資源 ClearFilterList(); // 刪除符號串連 RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME); IoDeleteSymbolicLink(&deviceLinkUnicodeString); // 刪除裝置對象 IoDeleteDevice(DriverObject->DeviceObject);}
我們已經編寫了驅動的主代碼,下面我們繼續實現Filter-Hook Driver
註冊過濾函數
在上面的代碼中,你已經看到了SetFilterFunction(..)函數。我在IP Filter Driver中執行這個函數來註冊過濾函數,步驟如下:
1) 首先,我們必須得到IP Filter Driver的指標,這要求驅動已經安裝並執行。為了保證IP Filter Driver已經安裝並執行,在我的使用者程式中,在載入本驅動前載入並啟動IP Filter Driver。
2) 第二步,我們必須建立用IOCTL_PF_SET_EXTENSION_POINTER作為控制碼的IRP。我們必須傳遞PF_SET_EXTENSION_HOOK_INFO 參數,該參數結構中包含了指向過濾函數的指標。如果你要卸載該函數,你必須在同樣的步驟裡傳遞NULL作為過濾函數指標。
3) 向裝置驅動發送建立IRP, 這裡有一個大的問題,只有一個過濾函數可以安裝,因此如果另外的應用程式已經安裝了一個過濾函數,你就不能再安裝了。
設定過濾函數的代碼如下:
NTSTATUS SetFilterFunction (PacketFilterExtensionPtr filterFunction){ NTSTATUS status = STATUS_SUCCESS, waitStatus=STATUS_SUCCESS; UNICODE_STRING filterName; PDEVICE_OBJECT ipDeviceObject=NULL; PFILE_OBJECT ipFileObject=NULL; PF_SET_EXTENSION_HOOK_INFO filterData; KEVENT event; IO_STATUS_BLOCK ioStatus; PIRP irp; dprintf("Getting pointer to IpFilterDriver/n"); //首先我們要得到IpFilterDriver Device的指標 RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME); status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL, &ipFileObject, &ipDeviceObject); if(NT_SUCCESS(status)) { //用過濾函數作為參數初始化PF_SET_EXTENSION_HOOK_INFO結構 filterData.ExtensionPointer = filterFunction; //我們需要初始化事件,用於在完成工作後通知我們 KeInitializeEvent(&event, NotificationEvent, FALSE); //建立用於設立過濾函數的IRP irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, ipDeviceObject, if(irp != NULL) { // 發送 IRP status = IoCallDriver(ipDeviceObject, irp); // 然後我們等待IpFilter Driver的回應 if (status == STATUS_PENDING) { waitStatus = KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); if (waitStatus != STATUS_SUCCESS ) dprintf("Error waiting for IpFilterDriver response."); } status = ioStatus.Status; if(!NT_SUCCESS(status)) dprintf("Error, IO error with ipFilterDriver/n"); } else { //如果不能分配空間,返回相應的錯誤碼 status = STATUS_INSUFFICIENT_RESOURCES; dprintf("Error building IpFilterDriver IRP/n"); } if(ipFileObject != NULL) ObDereferenceObject(ipFileObject); ipFileObject = NULL; ipDeviceObject = NULL; } else dprintf("Error while getting the pointer/n"); return status;}
當我們已經完成了建立過濾函數的工作,當取得裝置驅動的指標後必須釋放檔案對象。我使用事件來通知IpFilter Driver 已經完成了IRP處理。
過濾函數
我們已經知道了如何開發驅動並安裝過濾函數,但還不知道該過濾函數中的任何東西。當主機接收或發送一個資料包,該過濾函數總會被調用,系統會根據函數的傳回值決定如何處理這個資料包。
函數的原型是這樣的:
typedef PF_FORWARD_ACTION (*PacketFilterExtensionPtr)( // Ip資料包的包頭 IN unsigned char *PacketHeader, // 不包括頭的資料包 IN unsigned char *Packet, // 包長度。不包括IP頭的長度 IN unsigned int PacketLength, // 接收資料的介面適配器編號 IN unsigned int RecvInterfaceIndex, // 發送資料的介面適配器編號 IN unsigned int SendInterfaceIndex, //接收資料包的適配器IP地址 IN IPAddr RecvLinkNextHop, //發送資料包的適配器IP地址 IN IPAddr SendLinkNextHop );
PF_FORWARD_ACTION 是枚舉類型,可能的值有:
PF_FORWARD 指示IP filter Driver立即向IP棧傳遞資料。對於本機資料包,IP向上送入棧。如果目標是另外的機器並且允許路由,將通過IP路由發送。
PF_DROP 指示IP filter Driver丟棄IP包。
PF_PASS 指示IP filter Driver去過濾該資料包,IP filter Driver如果處理該資料包取決於包過濾API的設定。過濾鉤子返回pass的回應,表明它沒有處理該資料包而讓IP filter Driver去過濾該資料包。
雖然DDK文檔只包含了這三個值,如果你查看pfhook.h(Filter-Hook Driver需要的標頭檔)你可以發現還有一個PF_ICMP_ON_DROP,我猜這個值是用於對ICMP包進行丟棄。
在過濾函數的定義中,傳遞進來指向資料包頭的指標。因此,你可以修改資料頭然後發送。這對於進行NAT轉換是非常有用的,你可以修改資料包的目標地址,選擇IP路由。
在我的實現裡,過濾函數根據使用者程式的要求將每個包與規則列表進行比較,這個列表是串連列表,是在運行期間用START_IP_HOOK 的IOCTL建立的。在代碼裡可以看到這些。
原始碼
在第一個版本中我只包含了簡單的樣本,由於許多朋友要求我幫他們開發實際應用,我完善了這個例子。新的樣本是資料包的過濾程式,在這個新程式裡你可以像商業防火牆一樣添加你的過濾規則。
第一個版本中,應用程式由兩部分組成:
(一)使用者應用程式:這是管理過濾規則的MFC應用程式。程式發送規則給驅動並決定何時啟動過濾,有三個步驟:
1) 定義所需的規則。可以通過添加、刪除定製規則。
2) 安裝規則。定義好規則後,點install按鈕將他們發送給驅動
3) 啟動過濾。你可以單擊start按鈕啟動過濾
(二)Filter-Hook Driver: 根據從使用者應用程式中接收到的過濾規則對資料包進行過濾。 Filter-Hook Driver必須與使用者應用程式在相同的目錄中。
為什麼使用該方法開發防火牆
在Windows中這不是開發防火牆的唯一方法,其它的有諸如 NDIS防火牆,TDI防火牆,Winsock分層防火牆,包過濾API,...因此我將說明Filter-Hook Driver的優缺點使你在以後開發防火牆時決定是否採用這種驅動。
1) 這種方法所擁有的彈性可以使你過濾所有IP層(或以上)的通訊。但你不能過濾更低層的頭部資料,例如:你不能過濾以太幀資料。你需要用NDIS過濾器來做,雖然開發困難但彈性更大。
2) 這是一種簡單的方法。安裝防火牆和執行過濾功能非常簡單。但包過濾API(Packet Filtering API)更加容易使用,儘管它缺少彈性,例如你不能處理包的內容,你不能用包過濾API修改內容。
結論:Filter-Hook Driver不是最好但也不壞。但為什麼商業產品中並不使用它呢?
答案是簡單的:儘管這種驅動並不糟糕但卻有巨大的缺點:像我剛才所說的,每次只能有一個過濾函數可以安裝。我們開發了強大的防火牆,它可以被成千上萬個使用者下載,如果其它應用程式已經使用了過濾器(安裝了過濾函數)那麼我們的程式將不會正常工作。
這種方法的另外一個缺點是不被微軟官方明文支援。雖然DDK文檔上說你可以在過濾函數中處理包的內容, 但事實並非如此。你可以在接收資料包的時候處理包的內容,但在發送包的時候你僅能讀IP、TCP、UDP或ICMP資料包頭。我不知道這是為什麼....
微軟介紹在Windows XP中另外一種驅動沒有這樣的限制,那就是firewall hook driver。但微軟不推薦使用它,因為"it ran too high in the network stack"。或許這種驅動在以後的WINDOWS版本中會消失。
結束:
好的,我知道這不是開發防火牆的最好的方法(我剛才已經提及了它巨大的缺點),但我想這對於在研究這個或對這個有所興趣的人們是一個好的開始。希望你能夠讀明白,並開發出一款強大的防火牆。