在Windows 2000/XP下開發防火牆

Sample Image

介紹

如果你決定在 Linux 下開發一個防火牆,你可以找到很多資料和源代碼,全部免費。但對 Windows 平臺下的防火牆感興趣的人們就有些困難了,不僅是找資料難,找免費的源代碼更是個不可能的任務!

所以,我決定寫這片文章來簡述一下在 Windows 2000/XP 下開發防火牆的簡單方法,藉以幫助那些對此感興趣的人們。

背景

在 Windows 2000 DDK 中,微軟包含了一種新的網絡驅動叫做過濾鉤子驅動( Filter-Hook Driver )。使用它,你可以建立一個函數來過濾所有到達或離開接口的數據包。

因爲關於這個主題的文檔很少,並且沒有例子,所以我寫了這篇文章來介紹成功使用它所需的步驟。我希望這篇文章能幫你理解這個簡單的方法。

過濾鉤子驅動

正如我前面所說,過濾鉤子驅動是微軟在 Windows 2000 DDK 中引入的。實際上,它不是一個新的網絡驅動類,它只是擴展 IP 過濾驅動(包含在 Windows 2000 及以後的版本中)功能的一種方法。

事實上,過濾鉤子驅動不是網絡驅動,而是內核模式驅動。基本的,我們在過濾鉤子驅動中實現一個回調函數,然後把這個回調函數與 IP 過濾驅動註冊到一起。這樣做了之後,IP 過濾驅動會在一個數據包被髮送或接受時調用我們的回調函數。那麼……這麼做主要的步驟有哪些呢?

我總結了以下五個步驟:

  1. 建立一個過濾器鉤子驅動。這一步,你必須建立內核模式驅動,選擇一個名字,DOS 名和其它驅動字符,沒什麼特別的要求但我建議你用描述性的名字。
  2. 如果我們要安裝過濾函數,首先必須取得一個指向 IP 過濾驅動的指針。所以,這是第二個步驟。
  3. 我們已經有了指針,現在可以安裝過濾函數了。我們可以通過發送指定的 IRP 來做這件事。在傳遞的“消息”數據中包含指向過濾函數的指針。
  4. 過濾數據包!!!
  5. 當我們決定停止過濾時,必須註銷過濾函數。我們可以通過註冊過濾函數到空指針來實現註銷。

哦,只有五步,而且看起來很簡單,但是……要怎樣建立內核模式驅動呢?怎麼取得指向 IP 過濾驅動的指針?怎麼……是的,請稍等,我現在就來解釋這些步驟:P,展示源碼例子。

建立內核模式驅動

過濾鉤子驅動是內核模式驅動,所以如果我們想做,就得做個內核模式驅動。這篇文章並不是“5分鐘學會怎樣開發內核模式驅動”,所以我假設讀者們已經有了這方面的知識。

過濾鉤子驅動的結構是典型的內核模式驅動結構:

  1. 建立設備的驅動入口(注:建立設備是驅動入口的定語),設置標準例程來處理 IRP(分派,加載,卸載,創建……)和建立與其它應用程序通訊的符號連接。
  2. 管理 IRP 的標準例程。在你開始編寫代碼之前,我建議,考慮你要從設備驅動引出到應用程序哪些 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" );

//we have to create the device
RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME);

ntStatus = IoCreateDevice(DriverObject,
0 ,
&deviceNameUnicodeString,
FILE_DEVICE_DRVFLTIP,
0 ,
FALSE,
&deviceObject);



if ( NT_SUCCESS(ntStatus) )
{
// Create a symbolic link that Win32 apps can specify to gain access
// to this driver/device
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);

ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString,
&deviceNameUnicodeString);

//....

// Create dispatch points for device control, create, close.

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 code to start filtering
case START_IP_HOOK:
{
SetFilterFunction(cbFilterFunction);

break ;
}

// ioctl to stop filtering
case STOP_IP_HOOK:
{
SetFilterFunction(NULL);

break ;
}

// ioctl to add a filter rule
case ADD_FILTER:
{
if (inputBufferLength == sizeof (IPFilter))
{
IPFilter *nf;

nf = (IPFilter *)ioBuffer;

AddFilterToList(nf);
}

break ;
}

// ioctl to free filter rule list
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);

// We never have pending operation so always return the status code.
return ntStatus;
}


VOID DrvUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING deviceLinkUnicodeString;

dprintf("DrvFltIp.SYS: Unloading/n" );

SetFilterFunction(NULL);

// Free any resources
ClearFilterList();

// Delete the symbolic link
RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
IoDeleteSymbolicLink(&deviceLinkUnicodeString);


// Delete the device object
IoDeleteDevice(DriverObject->DeviceObject);
}
我們已經做好了驅動的主要代碼,接下來是過濾鉤子驅動的代碼。

註冊過濾函數

在上面的代碼中,你已經看到了一個叫做 SetFilterFunction(..) 的函數。實現這個函數是用來註冊函數到 IP 過濾驅動的。有下面的幾步:

  1. 首先,我們必須取得一個指向 IP 過濾驅動的指針。這要求 IP 過濾驅動已經安裝並運行了。我的用戶應用程序,在加載這個驅動之前就加載並啓動了 IP 過濾驅動,來保證這一點。
  2. 第二,我們必須建立一個 IRP 指定 IOCTL_PF_SET_EXTENSION_POINTER 作爲 IO 控制代碼。我們必須以參數的形式傳遞一個 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" );

//first of all, we have to get a pointer to IpFilterDriver Device
RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME);
status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL,
&ipFileObject, &ipDeviceObject);

if (NT_SUCCESS(status))
{
//initialize the struct with functions parameters
filterData.ExtensionPointer = filterFunction;

//we need initialize the event used later by
//the IpFilterDriver to signal us
//when it finished its work
KeInitializeEvent(&event, NotificationEvent, FALSE);

//we build the irp needed to establish fitler function
irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER,
ipDeviceObject,
if (irp != NULL)
{
// we send the IRP
status = IoCallDriver(ipDeviceObject, irp);

//and finally, we wait for
//"acknowledge" of 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
{
//if we cant allocate the space,
//we return the corresponding code error
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;
}

你會發現當我們完成了建立過濾函數的處理,我們必須廢除( de-reference )在我們取得指向設備驅動的指針時獲得的文件對象。我用了一個將在 IP 過濾驅動完成對 IRP 的處理時提示的事件。

過濾函數

我們已經看到了如何開發驅動和如何安裝過濾函數,但我們還不知道過濾函數是什麼樣的。

我已經說過了這個函數總是在主機接受或發送一個數據包時被調用。根據這個函數的返回值,系統來決定對這個包作些什麼。

這個函數的原型必須是這樣的:

typedef
  PF_FORWARD_ACTION 
(*PacketFilterExtensionPtr)(
// Ip Packet Header
IN unsigned char *PacketHeader,
// Packet. Don't include Header
IN unsigned char *Packet,
// Packet length. Don't Include length of ip header
IN unsigned int PacketLength,
// Index number for the interface adapter
//over which the packet arrived
IN unsigned int RecvInterfaceIndex,
// Index number for the interface adapter
//over which the packet will be transmitted
IN unsigned int SendInterfaceIndex,
//IP address for the interface
//adapter that received the packet
IN IPAddr RecvLinkNextHop,
//IP address for the interface adapter
//that will transmit the packet
IN IPAddr SendLinkNextHop
);

PF_FORWARD_ACTION 是一個枚舉類型,可以取值爲:

  • PF_FORWARD

    指定 IP 過濾驅動立即向 IP 棧返回 forward 響應。對於本地包,IP 將他們直接入棧。如果包的目的地是另一臺計算機並且允許路由,IP 將它們相應的路由。

  • PF_DROP

    指定 IP 過濾驅動立即向 IP 棧返回 drop 響應。IP 應該丟棄數據包。

  • PF_PASS

    指定 IP 過濾驅動過濾包並向 IP 棧返回結果響應。IP 過濾驅動如何處理過濾數據包取決於數據包過濾 API 的設置。

    如果過濾鉤子決定不處理這個包而交給 IP 過濾驅動來過濾這個包,返回這個 pass 響應。

雖然 DDK 文檔只包含了這三個值,但你查看 pfhook.h (過濾鉤子驅動需要包含)就會看到另一個值。這個值是 PF_ICMP_ON_DROP 。我估計這個值對應的是丟棄包並以一個 ICMP 包反饋錯誤信息。

正如你在過濾函數的定義中看到的,數據包及它的報頭是以指針傳遞的。所以,你可以修改報頭或負載然後傳遞該包。這是很有用的,例如做網絡地址轉換( NAT )。如果你改變了目標地址,IP 會路由該包。

在我的實現中,過濾函數將每個包和一個由用戶應用程序引入的規則列表比較。這個列表是用鏈表實現的,由每個 START_IP_HOOK IOCTL 在運行時建立。你可以在我的代碼中看到這些。

代碼

在這篇文章的第一個版本中包含了一個簡單的例子,但由於有人想讓我幫他開發實際的程序,我把它更新成了一個更復雜的例子。新的例子時一個小的數據包過濾程序。用這個新程序你可以實現你自己的過濾規則,就像你在一些商業防火牆軟件中做的那樣。

在第一個版本中,程序包含兩個組件:

  • 用戶應用程序:一個 MFC 應用程序用來管理過濾規則。這個應用程序發送規則到驅動程序並決定驅動什麼時候開始過濾。過濾數據分三步:
    • 定義你需要的規則。用添加和刪除命令你可以添加或刪除過濾規則。
    • 安裝規則。當你定義好了規則,點擊安裝按鈕,發送它們到驅動程序。
    • 開始過濾。你只需要點擊開始按鈕來開始過濾。
  • 過濾鉤子驅動:根據由用戶應用程序接收到的過濾規則來過濾 IP 數據包的驅動。

過濾鉤子驅動必須和用戶應用程序的可執行文件處在同一個目錄中。

爲什麼用這種方法開發防火牆?

這不是在 Windows 開發防火牆的唯一方法,還有其它的像 NDIS 防火牆,TDI 防火牆,Winsock 層的防火牆,數據包過濾 API,……所以我總結了一些過濾鉤子驅動的優點和缺點,以供你在將來需要用到時參考。

  • 用這種方法有很大的靈活性。你可以過濾所有的 IP 數據包(以及上層的數據包)。但你不能過濾更底層的報頭,例如,你不能過濾以太網幀。你需要用 NDIS 過濾來實現,更復雜也更靈活。
  • 這是一個簡單的方法。安裝防火牆和實現過濾函數用這種方法都是很簡單的。但數據包過濾 API 更爲簡單,雖然沒有它靈活。你不能訪問數據包的內容,也不能用數據包過濾 API 修改它。

結果:過濾鉤子驅動並不是最好的,但它也沒有什麼壞特性。可是爲什麼這個方法沒有用在商業產品中呢?

答案很簡單。雖然這個驅動沒有不好的特性,但它有一個很大的缺點。正如我之前提到的,每次只能安裝一個過濾函數。我們可以開發一個強大的防火牆,它可能被上千的用戶下載並安裝,但如果其它的應用程序使用了過濾(並在之前安裝了過濾函數),我們的程序就什麼都做不了了。

這個方法還有另一個缺點沒有被微軟的文檔提到。雖然 DDK 文檔說你可以在過濾函數中訪問數據包內容,但那不是真的。你可以訪問收到的包的內容,但對於發送的包,你只能讀 IP 和 TCP,UDP 或 ICMP 報頭。我不明白爲什麼……

微軟在 Windows XP 中引入了另一種沒有這個限制的驅動:防火牆鉤子驅動。它的安裝很類似,但微軟並不建議使用它,以爲“它消耗太多的網絡棧”。也許這個驅動會在以後的 Windows 版本中消失。

結論

好了,到結尾了。我只到這並不是開發防火牆的最好方法(我在前面提到了它的缺點)。但我認爲這對正在查找資料和對此感興趣的人們是個好的開始。

我希望你能得到些幫助,並開始想要開發個強大的防火牆了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章