|
論壇說明 |
歡迎您來到『史萊姆論壇』 ^___^ 您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的! 請點擊這裡:『註冊成為我們的一份子!』 |
|
主題工具 | 顯示模式 |
2003-12-14, 08:29 PM | #1 |
榮譽會員
|
套用層截包方案與實現
作者:Fang(fangguicheng@21cn.com)
為什麼要在套用層截包 引言 截包的需求一般來自於過濾、轉換傳輸協定、截取報文分析等。 過濾型的套用比較多,典型為包過濾型防火牆。 轉換傳輸協定的套用局限於一些特定環境。比如第三方開發網路傳輸協定軟體,不能夠與原有操作系統軟體融合,只好採取「嵌入傳輸協定棧的塊」(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同時紀錄網路事件; 正文完 |
送花文章: 3,
|