1. 程式人生 > >應用層截包介紹

應用層截包介紹

截包的需求一般來自於過濾、轉換協議、擷取報文分析等。
過濾型的應用比較多,典型為包過濾型防火牆。
轉換協議的應用侷限於一些特定環境。比如第三方開發網路協議軟體,不能夠與原有作業系統軟體融合,只好採取“嵌入協議棧的塊”(BITS)方式實施。比如IPSEC在Windows上的第三方實現,無法和作業系統廠商提供的IP軟體融合,只好實現在IP層與鏈路層之間,作為協議棧的一層來實現。第三方PPPOE軟體也是通過這種方式實現。
擷取包用於分析的目的,用“抓包”描述更恰當一些,“截包”一般表示有截斷的能力,“抓包”只需要能夠獲取即可。實現上一般作為協議層實現。
本文所說的“應用層截包”特指在驅動程式中截包,然後送到應用層處理的工作模式。

截包模式 

使用者態下的網路資料包攔截方式有
1.    Winsock Layered Service Provider;
2.    Windows 2000 包過濾介面;
3.    替換系統自帶的WINSOCK動態連線庫;

利用驅動程式攔截網路資料包的方式有
1.    TDI過濾驅動程式(TDI Filter Driver)
2.    NDIS中間層驅動程式(NDIS Intermediate Driver)
3.    Win2k Filter-Hook Driver
4.    NDIS Hook Driver

使用者態下攔截資料包有一些侷限性,“很顯然,在使用者態下進行資料包攔截最致命的缺點就是隻能在Winsock層次上進行,而對於網路協議棧中底層協議的資料包無法進行處理。對於一些木馬和病毒來說很容易避開這個層次的防火牆。”
    我們所說的“應用層截包”不是指上面描述的在使用者態攔截資料包。而是在驅動程式中攔截,在應用層中處理。要獲得一個通用的方式,應該在IP層之下進行攔截。綜合比較,本文選用中間層模式。

為什麼要在應用層處理擷取的報文

一般來說,網路應用如防火牆,協議類軟體都是工作在核心,我們為什麼要反過來,提出要在應用層處理報文呢?理由也可以找出幾點(哪怕是比較牽強):
    眾所周知,驅動程式開發有一定的難度,對於一個經驗豐富的程式設計師來說,或許開發過程中不存在技術問題,但是對初學者,尤其是第一次接觸的程式設計師簡直是痛苦的經歷。
    另外,開發週期也是一個不得不考慮的問題。程式工作在核心,穩定性/相容性都需要大量測試,而且可供使用的函式庫相對於應用層來說相當少。在應用層開發,除錯修改相對要容易地多。
    不利的因素也有:
    效能影響,在應用層工作,改變了工作模式,每當驅動程式截到資料,送到應用層處理後再次送回核心,再向上傳遞到IP協議。因此,效能影響非常大,效率非常低,在100Mbps網路上,只有80%的效能表現。
    綜合來看,在特定的場合應用還是比較適合的:
    桌上型電腦上使用,桌上型電腦的網路負載相當小,不到100Mbps足以滿足要求,尤其是主要用於上網等環境,網路連線的流量不到512Kbps,根本不用考慮效能因素。作為單機防火牆或其他一些協議實現,分析等很容易基於這種方式實現。

方案

模型

    上圖描述了應用層截包的模型,主要的流程如下:

接收報文過程:
1.    網路介面收到報文,中間層擷取,通過2送到應用層處理;
2.    應用層處理後,送回中間層處理結果;
3.    中間層根據處理結果,丟棄該報文,或者將處理後的報文通過1送到IP協議;
4.    IP協議及上層應用接收到報文;

傳送報文過程:
1.    上層應用傳送資料,從而IP協議傳送報文;
2.    報文被中間層擷取,通過2送到應用層處理;
3.    應用層處理後,送回中間層處理結果;
4.    中間層根據處理結果,丟棄該報文,或者將處理後的報文傳送到網路上;

實現細節探討

IO與通訊 

有一個很容易的方式,在驅動程式和應用程式之間用一個事件。
在應用程式CreateFile的時候,驅動程式IoCreateSynchronizationEvent一個有名的事件,然後應用程式CreateEvent/OpenEvent此有名事件即可。
注意點:
1,    不要在驅動初始化的時候建立事件,此時大多不能成功建立;
2,    讓驅動先建立,那麼此後應用程式開啟時,只能讀(Waitxxxx),不能寫(SetEvent/ResetEvent)。反之,如果應用程式先建立,則應用程式和驅動程式都有讀寫許可權;
3,    用名字比較理想,注意驅動中名字在/BaseNamedObjects/下,例如應用程式用“xxxEvent”,那麼驅動中就是“/BaseNamedObjects/xxxEvent”;
4,    用HANDLE的方式也可以,但是在WIN98下是否可行,未知。
5,    此後,驅動對讀請求應立即返回,否則就返回失敗。不然將失去用事件通知的意義(不再等待讀完成,而是有需要(通知事件)時才會讀);
6,    應用程式發現有事件,應該在一個迴圈中讀取,直到讀取失敗,表明沒有資料可讀;否則會漏掉後續資料,而沒有及時讀取;

處理執行緒優先順序

    應用層處理執行緒應該提高優先順序,因為該執行緒為其他上層應用程式服務,如果優先順序比其他執行緒優先順序低的話,將會發生類似死鎖的等待狀態。
    另外,提高優先順序的時候必須注意,執行緒儘量縮短執行時間,不要長期佔用CPU,否則其他執行緒無法得到服務。優先順序不必提高到REALTIME_PRIORITY_CLASS級,此時執行緒不能做一些磁碟IO之類的操作,而且也影響到滑鼠、鍵盤等工作。
    驅動程式也可以動態地提高執行緒的優先順序。

快取

    在驅動程式接收到報文後,至少應該有一個緩衝以便臨時儲存,等待應用層處理。緩衝不必很大,只要能在應用層得到時間片之前緩衝區不溢位就可以了,實踐中大約能儲存幾十個報文就夠了。
    緩衝的使用方式,是一個先進先出的佇列。考慮方便實現為靜態儲存的環形佇列,也就是說,不必每次分配記憶體,而是一次性分配好一大塊記憶體,環形的使用。
初始,head==tail==0;
tail和head都是無限增長的。
Tail – head <= size;

放入一個報文時, tail=tail + packetlen;
取出一個報文時,head=head + packetlen;

tail== head表明空;
tail>head表明有資料;
tail + input packet length - head >size表明滿;

取資料時:
ppacket GetPacket()
{
    ASSERT(tail>=head);

    if(tail==head)
        return NULL;

//else
    ppacket = &start[head % SIZE];
if(head % size + ppacket->length > size )
    //資料不連續(一部分在尾部,一部分在頭部);
else
    //資料是連續的

return ppacket;
}

放入資料:
bool InputPacket(ppacket)
{
    if(tail + input packet length - head >size) //滿
        return false;

    //copy packet to &start[tail % SIZE]
    //if(tail % SIZE + packet length > SIZE)
        //資料不連續(一部分在尾部,一部分在頭部);
//else
        //資料是連續的

    tail = tail + packet length;
    return true;
}

上面這種方式採用陣列的方式組織,為每個報文提供一個最大報文長度的空間。因為緩衝區數目有限,因此這種方式可以滿足需要。如果要考慮到減少空間的浪費,那麼可以按每個報文的實際長度儲存,上面的演算法不能夠適應這種方式。

應用層和驅動程式的通訊

在網絡卡接收/IP傳送過程中,驅動程式快取報文,用事件通知應用層有報文需要處理。那麼應用層可以通過IO方式或者共享記憶體方式取得此報文。
實踐說明,在100Mbps速率下,以上兩種方式都可以滿足需要,最為簡便的方式就是使用有緩衝的IO方式。
應用層處理完畢,也可以使用以上兩種方式之一來向驅動程式遞交結果。不過,IO方式因為一次只能傳送一個報文,100Mbps網路速度下降為70%~80%網路速度,10Mbps不會有影響。也就是說,主機發出的最大速度只有70%的網路速度,這和應用程式傳送不超過MTU的UDP資料報的速度是一樣的。對TCP來說,由於是雙向通訊,損失更加大一些,大約40%~60%速度。
這時候,使用共享記憶體方式,因為減少了系統呼叫的開銷,可以避免速度下降。

報文傳送的速度控制

    當IP協議傳送報文的時候,一般來說,我們的中間層驅動必須把這些報文快取起來,告訴IP軟體傳送成功,然後讓應用層處理完畢之後再做決定。顯然,儲存報文的速度遠遠超過網絡卡能夠傳送的速度,然而IP軟體(特別是UDP)將以我們儲存的速度傳送報文。造成快取迅速耗盡。後續的報文只好丟棄。這樣一來,UDP傳送將不能正常工作。TCP由於可以自行適應網路狀況,依然可以在這種情況下工作,速度在70%左右。在Passthru裡,可以轉發至低層驅動,然後用非同步或同步方式返回,從而達到網絡卡的傳送速度一致。
    因此,必須有一個辦法避免這種狀況。中間層驅動把這些報文快取起來,告訴IP軟體傳送狀態未決(Pending)。等到最後處理完畢,告訴IP軟體傳送完成。從而協調了傳送速度。這種方式帶來一個問題,就是驅動程式必須在傳送超時的情況下放棄對這些緩衝報文的所有權。具體來說,就是MiniportReset被呼叫的時候,就有可能是NDIS察覺到傳送超時,從而放棄所有未完成的傳送操作,如果沒有正確處理這種情況,將會導致嚴重問題。如果中間層在Miniport初始化的時候通過呼叫NdisMSetAttributesEx函式設定了NDIS_ATTRIBUTE_IGNORE_PACKET_TIMEOUT標誌,那麼中間層驅動程式將不會得到報文超時通知,中間層必須自行處理快取的報文。

與Passthru協同工作

    當上層應用不再需要截包時,驅動程式應該完全是Passthru行為。這就要求所有傳送/接收函式應該正確處理在截包與非截包狀態,不至於做出危害行為。
    具體來說,在從NIC上接收/傳送,向IP協議提交資料包/接受IP協議傳送四個方向上正確處理所有接收/傳送函式。

其它輔助設施

    新增一些控制功能提供更細粒度的控制,讓應用程式獲得更多的自由。比如,可以控制擷取哪一個網絡卡,可以控制擷取某個方向上的流量,網路是否有變化(網絡卡解除安裝/Disable)等等。

實現

    實現選擇Passthru原始碼,在其上進行修改,主要修改包括:
1.    修改接收函式
2.    修改傳送函式
3.    增加報文快取
4.    增加IO部分
5.    增加控制功能
6.    增加應用層處理後的後續處理

這個實現使用了共享記憶體方式,具有一個處理前緩衝池和一個應用程式處理後的緩衝池。由於接收報文和待發送報文使用同一個緩衝池,也因為其他一些原因,這個實現的傳送效率並沒有比用IO方式快多少。
通過精心的設計和比較,完全可以做到100Mbps的收發速度。
這份文章旨在討論這種應用層截報的工作方式和可行性。也由於驅動程式原始碼並沒有經過特別嚴格的測試,不適合商業使用,作為示範,也僅僅對乙太網型別的報文進行了攔截。因此示範的驅動程式將不包含原始碼。

API說明

第三方開發使用cap.h標頭檔案,capdll.dll包含了下列函式:
BOOL CapInitialize();
VOID CapUninitialize();
BOOL CapStartCapture(PKTPROC PacketProc, ADAPTERS_CHANGE_CALLBACK AdaptChange);
VOID CapStopCapture();
DWORD CapGetAdaptList(PADAPT_INFO pAdaptInfo, DWORD BufferSize);
VOID CapSetRule(HANDLE Adapter, ULONG Rule);
BOOL CapSendPacket(HANDLE Adapter, ULONG Opcode, ULONG Length, PUCHAR Data);
同時提供了該dll的capdll.lib檔案以便在vc工程檔案中引入capdll.lib使用更為方便的編譯連線方式。

說明

所有函式的返回值都沒有指明錯誤原因。DEBUG版本可以在控制檯打印出執行資訊,並且在C:/ capture.txt有同樣的輸出資訊。

BOOL CapInitialize();
說明:
通知截報中間層驅動做一些必要的初始化工作。
引數:
無。
返回值:
失敗返回FALSE。

VOID CapUninitialize();
說明:
釋放驅動程式建立的事件,執行緒,記憶體等。
引數:
無。
返回值:
無。
注意:
    在呼叫此函式之前,應當呼叫CapSetRule將驅動程式截報規則設定成Opcode_PASSTHRU,以便恢復PASSTHRU行為。

BOOL CapStartCapture(PKTPROC PacketProc, ADAPTERS_CHANGE_CALLBACK AdaptChange); 說明:
啟動截報。Capdll將會建立一個執行緒,執行在THREAD_PRIORITY_HIGHEST優先順序,並等待網路事件,當有驅動程式接收到報文,或者IP協議傳送報文,或者發現網絡卡啟動/禁用/插入/拔除等,將會通過使用者提供的回撥函式通知使用者。
引數:
    PacketProc:使用者提供的報文處理函式;
    AdaptChange:使用者提供的網路變化通知函式;
返回值:

VOID CapStopCapture();
說明:
    停止截報。銷燬建立的執行緒。
引數:
    無。
返回值:
    無。

DWORD CapGetAdaptList(PADAPT_INFO pAdaptInfo, DWORD BufferSize);
說明:
    獲取網路適配卡列表。
引數:
    pAdaptInfo ADAPT_INFO結構陣列,使用者提供足夠的空間。
    BufferSize 緩衝區尺寸。
返回值:
    網路適配卡數目。

VOID CapSetRule(HANDLE Adapter, ULONG Rule);
說明:
    設定截報規則。
引數:
    Adapter:指定擷取的網絡卡控制代碼。
    Rule:為Opcode_PASSTHRU:PASSTHRU行為;Opcode_SND:擷取所有傳送報文;Opcode_RCV:擷取所有接收報文。可以使用Opcode_SND | Opcode_RCV。
返回值:
    無。

BOOL CapSendPacket(HANDLE Adapter, ULONG Opcode, ULONG Length, PUCHAR Data);
說明:
    將處理後的報文放入緩衝區。也可以自行構造報文。不僅可以傳送報文,也可以將報文送給本機IP軟體。
引數:
    Adapter:指定使用的網絡卡控制代碼。
    Opcode:Opcode_SND,將報文傳送到網路上;Opcode_RCV,將報文傳遞給本機軟體。
    Length:報文長度;
    Data:報文內容;
返回值:
    成功返回TRUE,失敗返回FALSE。

Sample

#include "cap.h"
#include <stdio.h>

// global data.
ADAPT_INFO AdaptInfo[16];
int AdapterNum;

VOID PacketProc(HANDLE Adapter, ULONG Opcode, ULONG Length, PUCHAR Data)
{
    CapSendPacket(Adapter, Opcode, Length, Data);
}

VOID AdaptersChangeCallback()
{
    AdapterNum = CapGetAdaptList(AdaptInfo, sizeof(AdaptInfo));
}

int main(int argc, char* argv[])
{
    BOOL bRet;
    char cmd[80];
    int i;

    bRet = CapInitialize();
    if(bRet)
    {
        AdapterNum = CapGetAdaptList(AdaptInfo, sizeof(AdaptInfo));
        
        for(i=0; i<AdapterNum; i++)
        {
            CapSetRule(AdaptInfo[i].Adapter, Opcode_SND | Opcode_RCV);
        }

        CapStartCapture(PacketProc, AdaptersChangeCallback);

        for(;;)
        {
            gets(cmd);
            if(strcmp(cmd, "quit")==0)
            {
                break;
            }
        }

        for(i=0; i<AdapterNum; i++)
        {
            CapSetRule(AdaptInfo[i].Adapter, Opcode_PASSTHRU);
        }

        CapStopCapture();
        CapUninitialize();
    }

    return 0;
}

應用舉例

    上述程式碼做了一個Passthru行為。
    作網橋或者NAT,需要在報文處理函式裡,將報文內容根據需要修改乙太網頭部或其他行為,然後從合適的另一塊網絡卡上發出去;
    作協議轉換,比如IP/UDP隧道或者複雜如IPSEC之類,可以在報文處理函式裡將報文內容解開隧道或者解密,重新組報文,放入緩衝區,讓驅動程式送到IP軟體;
作防火牆,根據規則,丟棄不受歡迎的報文,正常的報文同樣PASSTHRU;
作入侵監測/安全審計(當然只能保護本機),PASSTHRU同時紀錄網路事件;