介紹
如果你決定開發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版本中會消失。
結束:
好的,我知道這不是開發防火牆的最好的方法(我剛才已經提及了它巨大的缺點),但我想這對於在研究這個或對這個有所興趣的人們是一個好的開始。希望你能夠讀明白,並開發出一款強大的防火牆。
本文配套源碼