如何實現防火牆鉤子驅動

Sample Image - FwHookDrv.jpg

介紹

也許,防火牆鉤子驅動是在 Windows 系統下開發數據包過濾程序的方法中最沒有文檔可查的一個了。微軟沒有給出關於它的任何文檔,你唯一能夠找到點兒什麼的地方是 DDK 頭文件(ipFirewall.h)。事實上,當我安裝了 Windows 2000 DDK,我對於發現這個 .h 文件(和它的內容)非常驚奇,因爲沒有文檔提到防火牆鉤子的存在。在下一個版本的 DDK 中,微軟加了一些關於它的文檔:“這個方法存在但不建議使用”。

然而,因爲它是個實現防火牆的簡單方法,我認爲了解防火牆鉤子驅動如何工作將是很有趣的。

防火牆鉤子驅動

我不明白爲什麼微軟不建議使用防火牆鉤子驅動的開發。對於開發一個完整的防火牆方案我的確不建議用它,但對於小程序,它可是個好的選擇。基本上,防火牆鉤子驅動能夠做過濾鉤子驅動(見我的文章《在Windows 2000/XP下開發防火牆》 )所做的相同工作,但限制更少。

可能你還記得,過濾鉤子函數僅允許在系統中安裝一個過濾函數。如果有程序已經使用了這個功能,你的程序就沒用了。使用防火牆鉤子驅動,你不會遇到這個問題。你可以安裝你需要的所有過濾函數。每個過濾函數被賦予一個優先級,於是系統會一個接一個的(以優先級順序)調用過濾函數知道一個函數返回“DROP PACKET(丟棄包)”。如果所有的函數都返回“ALLOW PACKET(允許包)”,這個包就被允許通過了。你可以把它想象稱一個過濾函數鏈。當其中一個返回了“DROP PACKET”鏈就被切斷了。鏈中每個函數的順序是由它的優先級值數給出的。

Chained function figure

在上圖中,我表示了以下過程:

  1. 你的主機收到了一個包。IP 驅動擁有以優先級排列的過濾函數列表(具有更高優先級的函數是 Filter Function 1)。
  2. 首先,IP 驅動將包傳遞給優先級最高的過濾函數並等待返回值。
  3. Filter function 1 返回“ALLOW PACKET”。
  4. 因爲 Filter function 1 允許了包,IP 驅動將包傳遞給了下一個過濾函數:Filter function 2。
  5. 在圖示情況下,Filter function 2 返回了“DROP PACKET”。所以,IP 驅動丟棄了包而不在繼續調用下一個過濾函數。

你會發現的過濾鉤子驅動的另一個問題是對於包的發送,你不能訪問包中的數據內容。但是,用防火牆鉤子驅動你可以訪問所有的數據。防火牆鉤子過濾函數接收到的數據結構比過濾鉤子驅動的更復雜。它更接近於 NDIS 驅動中包的結構,整個的包是由一個緩衝鏈組成的。但是請耐心點,你將在後面看到更多。

像過濾鉤子驅動一樣,防火牆鉤子驅動只是個用來安裝回調函數的內核模式驅動(但防火牆鉤子驅動在 IP 驅動中安裝回調函數)。實際上,安裝防火牆鉤子驅動的過程與安裝過濾鉤子驅動類似。在 ipFirewall.h 文件中,你會看到下面的內容:

typedef
 struct
 _IP_SET_FIREWALL_HOOK_INFO 
{
// Packet filter callout. 
IPPacketFirewallPtr FirewallPtr;

// Priority of the hook
UINT Priority;

       // if TRUE then ADD else DELETE 
       BOOLEAN Add;
} IP_SET_FIREWALL_HOOK_INFO, *PIP_SET_FIREWALL_HOOK_INFO;

#define DD_IP_DEVICE_NAME L//Device//Ip
#define _IP_CTL_CODE(function, method, access) /
CTL_CODE(FSCTL_IP_BASE, function, method, access)
#define IOCTL_IP_SET_FIREWALL_HOOK /
_IP_CTL_CODE(12 , METHOD_BUFFERED, FILE_WRITE_ACCESS)

這幾行代碼告訴你了安裝回調函數的大致方法。你只需用你的回調函數的數據填寫 IP_SET_FIREWALL_HOOK_INFO 結構並安裝它發送 IOCTL IOCTL_IP_SET_FIREWALL_HOOK 到 IP 設備。簡單,如果你與驅動程序打過交道或是曾做過那個有文檔的過濾鉤子函數。這個結構的一個重要參數是優先級域。每個優先級域包含了該過濾函數的優先級,其值越來越大。

PDEVICE_OBJECT ipDeviceObject=NULL; 
IP_SET_FIREWALL_HOOK_INFO filterData;

//.....

// Init structure filterData.
FirewallPtr = filterFunction;
filterData.Priority = 1 ;
filterData.Add = TRUE;

//....

// Send the commando to ip driver
IoCallDriver(ipDeviceObject, irp);
如果你要卸載過濾函數,可以用同樣的代碼,只要把 filterData.Add 的值改成 FALSE 就行了。

過濾函數

防火牆鉤子驅動的過濾函數比過濾鉤子驅動的更復雜。因爲沒有關於該函數及其參數的文檔,所以複雜度增加了。該函數具有以下特徵:

FORWARD_ACTION cbFilterFunction(VOID **pData, 
UINT RecvInterfaceIndex,
UINT *pSendInterfaceIndex,
UCHAR *pDestinationType,
VOID *pContext, 
UINT ContextLength,
struct IPRcvBuf **pRcvBuf);

用耐心和調試方法(還有參數名字的解釋:)),我得出了以下關於參數的信息:

pData *pData 指向一個帶有包緩衝的 (struct IPRcvBuf *) 結構。
RecvInterfaceIndex 數據接收的接口。
pSendInterfaceIndex 指向無符號整型的指針,包含數據發送的索引值。雖然它是個指針,改變它的值並不能讓包改變路徑:(。
pDestinationType 指向無符號整型的指針,指出目標類型:本地網絡,遠程,廣播,多播,等。
pContext 指向 FIREWALL_CONTEXT_T 結構的指針,使你可以像流入和流出的包一樣取得包的信息。
ContextLength 指向的緩衝區的大小。其值總是 sizeof(FIREWALL_CONTEXT_T)。
pRcvBuf *pRcvBuf 總是指向 NULL。

這些信息可能會在將來的 Windows 版本中改變,因爲沒有官方的文檔可用。我只能保證這是我在 Windows 2000 和 Windows XP 下測試得到的這些域的意義。

對於每個包,我們的函數會被調用,並且取決於它的返回值,包會被丟棄或是放行。在過濾函數中你可以返回的值有:

FORWARD 包被允許。
DROP 包被丟棄。
ICMP_ON_DROP 包被丟棄並有一個 ICMP 包被髮送到遠程主機。

釋放緩衝

在防火牆鉤子過濾函數中,你並不是像在過濾鉤子驅動中那樣直接接收帶有包頭部和內容的緩衝區。在一些測試後,我弄清了緩衝的內部結構。如我前面所說,發送/接收 包是被傳遞給 pData 參數的。*pData 指向一個 IPRcvBuf 結構:

struct
 IPRcvBuf 
{
// Point to the next buffer in the chain
struct IPRcvBuf *ipr_next;

// Always 0 
UINT ipr_owner;

// Buffer data
UCHAR *ipr_buffer;

// Buffer data size
UINT ipr_size;

// In my tests always a pointer to NULL.
// Maybe the system could use MDLs instead of IPRcvBuf structures (but
// i never have seen it).
PMDL ipr_pMdl;

// Always a pointer to NULL.
UINT *ipr_pClientCnt;

// Always a pointer to NULL.
UCHAR *ipr_RcvContext;

// Always 0. I suppose this field is a offset into buffer data
// but because I haven't a value different from 0, I can affirm it.
UINT ipr_RcvOffset;

// In Windows 2003 DDK the name of this field have changed to flags.
// In my tests I always get 0 value for local traffic and 2 for remote.
// It's the only thing I can tell you about this field.
       ULONG ipr_promiscuous;
};

按照我們的意圖,只需要知道域 ipr_next, ipr_buffer 和 ipr_size。ipr_buffer 域包含包的 ipr_size 字節。然而,整個包不必在一個緩衝裏,系統能夠鏈接多個緩衝。正因如此,ipr_next 域被使用了。這個域指向了下一個帶有包數據的結構。當數據結構中的 ipr_next 指向 NULL 時我們就得到了整個的數據包。所以,我們在防火牆鉤子驅動中發現了鏈接緩衝結構正如在 NDIS 驅動中所見。在我的測試中,對於所有接收的包,函數只接收到了一個結構,在其緩衝中帶有全部的數據。對發送的包,我發現若干個鏈接的緩衝,每個包含一種協議的信息。我的意思是,例如我發送一個 ICMP 包,就會有三個鏈接的緩衝:一個帶有 IP 頭部,一個帶有 ICMP 頭部,而另一個帶有數據。然而,就像我們對 NDIS 驅動做的一樣,我們不能依賴於系統如何填寫這些緩衝。

在下面的圖中,你可以看到在防火牆鉤子驅動中如何構建數據包的例子:

Chained buffers figure

簡單的,你可以由下面的代碼看到如何從鏈接緩衝中得到一個帶有數據包內容的正統緩衝:

char
 *pPacket = NULL;
int iBufferSize;
struct IPRcvBuf *pBuffer = (struct IPRcvBuf *) *pData;

// First, I calculate the total size of the packet
iBufferSize = buffer->ipr_size;
while (pBuffer->ipr_next != NULL)
{
pBuffer = pBuffer->ipr_next;
iBufferSize += pBuffer->ipr_size;
}

// Reserve memory to the lineal buffer.
pPacket = (char *) ExAllocatePool(NonPagedPool, iBufferSize);
if (pPacket != NULL)
{
unsigned int iOffset = 0 ;
pBuffer = (struct IPRcvBuf *) *pData;

// we are going to copy each buffer of the chain in the lineal buffer.
memcpy(pPacket, pBuffer->ipr_buffer, pBuffer->ipr_size);
while (pBuffer->ipr_next != NULL)
{
iOffset += pBuffer->ipr_size;
pBuffer = pBbuffer->ipr_next;
memcpy(pPacket + iOffset, pBuffer->ipr_buffer,
pBbuffer->ipr_size);
}
}

還有,對於所有好奇的人(在你問到我之前:P),你可以修改包的數據,風險自己承擔。沒有任何工具來做這類的軟件,要做類似的東西,我建議你實現一個 NDIS IM 驅動或者 TDI 過濾驅動。我沒有做太多測試但我不很信賴防火牆鉤子驅動改變數據包內容的穩定性。爲什麼?因爲我們不知道 IP 驅動怎樣管理這些緩衝和修改它們要冒多大的風險。一句話,我建議:只許看,不許摸。

結合的時候到了!

好了,現在我們知道了過濾函數的語法和傳給它的包的格式。現在,我們要知道的就剩如何把這兩樣東西結合成一個數據包過濾程序了。我用的方法是試着定義一個類似於過濾鉤子驅動中用到的過濾函數,因爲這樣更容易理解。由於傳給過濾函數的參數在這兩個驅動中是不一樣的,對於防火牆鉤子,我實現了一箇中間函數(實際的防火牆鉤子過濾函數)來包裹過濾函數。我的意思是,在防火牆鉤子過濾函數中,我用了一個函數來處理包,將它拷貝到一個正統的緩衝中,然後把它傳遞給過濾函數。有下面的代碼,我想你能更好的理解它:

FORWARD_ACTION cbFilterFunction(VOID **pData,
UINT RecvInterfaceIndex,
UINT *pSendInterfaceIndex,
UCHAR *pDestinationType,
VOID *pContext,
UINT ContextLength,
struct IPRcvBuf **pRcvBuf)
{
FORWARD_ACTION result = FORWARD;
char *pPacket = NULL;
int iBufferSize;
struct IPRcvBuf *pBbuffer =(struct IPRcvBuf *) *pData;
PFIREWALL_CONTEXT_T fwContext = (PFIREWALL_CONTEXT_T)pContext;

IPHeader *pIpHeader;

// Convert chained buffer to lineal buffer as we see before.
// This won't be the fastest code but
// will help us to understand better the method.

// ...........

pIpHeader = (IPHeader *)pPacket;

// Call the real filter function and return result     
result = FilterPacket(pPacket,
// length in bytes = ipp->headerLength * (32 bits/8)
pPacket + (pIpHeader ->headerLength * 4 ),
iBufferSize - (pIpHeader ->headerLength * 4 ),
(fwContext != NULL) ? fwContext->Direction: 0 ,
RecvInterfaceIndex,
(pSendInterfaceIndex != NULL) ? *pSendInterfaceIndex : 0 );

return result;
}

代碼

你能很快認出這篇文章的程序。是的,圖形界面和我在過濾鉤子驅動中用的完全一樣。爲什麼?因爲我寫了個簡單的數據包過濾程序用來測試我寫的所有防火牆方法。這樣,我有了一個通用的圖形界面給它們,來提供相同的功能,但在底層,它們工作的不一樣。我有這個程序的不同版本(只做最小的改動)來測試我的過濾鉤子驅動,防火牆鉤子驅動,LSP DLL,TDI 過濾驅動,NDIS 驅動……所以,我想這個方法是好理解的。幾乎不更改圖形界面,這樣你只想瞭解使用的新方法。

像我其它的文章一樣,這個程序只實現了包的過濾。很多人問我關於加入一些更多的功能,像包記錄,安裝爲服務……但我想要按照提供方法而不是答案的思想。如果你想要加入一些這樣的功能,我很高興:)。你可以聯繫我,問所有你想要的問題。

結論

好了,一旦我寫完了這篇文章,它就是你的了。我希望你能從中學到與我一樣多的東西。Enjoy it!!

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