一、前言
Snort是一款基于C語言開發的,開源輕量級網絡入侵檢測系統,雖然輕量但是功能強大,最近因為學校項目的緣故,對snort的源碼進行了一些粗淺的分析,略有收獲,作為筆記記錄下來,同時也希望本文對大家有所幫助。
這是snort的官方網站:https://www.snort.org/
snort的基本使用方法和基礎知識,網上資料豐富,這里就不再贅述了。
所以我們直入主題,也就是snort數據包預處理模塊的源碼分析,注意本文所分析的源碼是snort2.2版本的,最新的2.9版本里面的函數模塊和某些數據結構有所變動,但是基本上的功能還是一致的。
二、前置知識
一般情況下數據包在進入檢測引擎之前都需要進行預處理,因為傳入的數據包是分片的需要重組,然后HTTP請求的URL字符串需要同一格式化,一些可疑的行為需要探測等等。如果單純依靠規則匹配,很難完成這些工作,所以snort就引入了分片重組,BO后門檢測等等預處理功能。
snort功能十分強大,但是其本身的功能其實非常簡單,就是嗅探數據包。那么這些豐富的功能是怎么實現的呢?snort采用了插件的機制拓展其本身的功能,各種各樣豐富的插件造就了snort的功能強大。
所以我們接下來要分析的數據包預處理模塊也是利用各種預處理插件實現的,因此我們對數據包預處理模塊的源碼分析,實際上是對預處理插件源碼以及snort與插件交互的源碼的分析。
三、源碼分析樹狀圖
以上是我對snort數據包預處理模塊源碼分析的流程及其大概實現思路流程圖,接下去我將會逐部分描述這張樹狀圖,以分析snort源碼。
四、預處理插件的基本工作流程
如上圖,預處理插件主要分為三部分,注冊、初始化和調用。
首先是注冊,snort里會先調用一個InitPreprocessors()函數對所有預處理插件進行初始化,其實這個函數并沒有實現本質的功能只是人為的將許多SetupXXX()函數組合在一起,這里的XXX表示不同的預處理插件的具體插件名。
InitPreprocessors()函數源碼如下:
void InitPreprocessors()
{
if(!pv.quiet_flag)
{
LogMessage("Initializing Preprocessors!\n");
}
SetupPortscan();
SetupPortscanIgnoreHosts();
SetupRpcDecode();
SetupBo();
SetupTelNeg();
SetupStream4();
SetupFrag2();
SetupARPspoof();
SetupConv();
SetupScan2();
SetupHttpInspect();
SetupPerfMonitor();
SetupFlow();
}
SetupFrag2()函數源碼如下:
void SetupFrag2()
{
RegisterPreprocessor("frag2", Frag2Init);
DEBUG_WRAP(DebugMessage(DEBUG_FRAG2, "Preprocessor: frag2 is setup...\n"););
}
然后InitPreprocessors()函數會根據不同的插件名,調用不同的SetupXXX()函數,在SetupXXX()函數中會調用RegisterPreprocessor()函數進行實際的注冊操作。將對應插件的插件名和對應的初始化函數進行封裝,并通過一個PreprocessNode節點指向它,將其鏈入到PreprocessorKeywordlist鏈表中,以便之后的遍歷使用。
需要注意的是,在InitPreprocessors()函數中只是進行了注冊,并沒有進行初始化,而是將初始化函數封裝了起來。因為snort中插件有很多,但是并不一定全部會用到,所以如果一開始就對所有的插件進行初始化就會導致程序的效率降低。因此snort將初始化推遲到調用的之前,只有我們用到了這個插件,才會對其進行初始化,并且采用了注冊的機制,方便快速的找到對應的插件名和對應的初始化函數。
初始化的具體流程:
snort允許用戶在snort.conf文件中對預處理插件進行配置,所以程序必須從snort.conf將配置信息讀取出來,分析參數傳給對應的預處理插件。
所以一開始主函數會調用ParseRuleFile()函數用于將snort.conf文件中的配置一行行的讀取出來并交由ParseRule()函數解析和拆分。
ParseRule()函數會分析每一行的配置,判斷配置的類型,如果是預處理插件配置會交給ParsePreprocessor()函數進一步分析配置參數。
ParsePreprocessor()函數會取出配置文件中預處理插件的插件名關鍵字,然后遍歷PreprocessorKeywordlist鏈表找到對應的節點,并把配置參數交給對應的初始化參數來完成。
在初始化函數中還會三個AddFuncToXXXlist()函數,用于將不同的作用的函數添加到不同的鏈表中。比如AddFuncToPreprocList()函數就是將實現插件主功能函數添加到Preprocess鏈表中去,便于之后的調用。后面兩個AddFuncToXXXlist()函數也是同理,根據字面意思就可以理解。
在初始化函數中已經將插件的主功能函數添加到了Preprocess鏈表中,所以會調用Preprocess()函數進行調用。不同的插件實現方式不同,之后會詳細舉例進行分析。
五、分片重組——Frag2插件分析
因為不同的網絡硬件設備往往有一個最大傳輸單元(MTU),以太網的MTU為1500字節,而IP包的大小最大為0xFFFF字節,所以大于1500字節的IP包會被分片,到對端重組。這個過程中可能攻擊者將特征匹配字符拆開繞過IDS的檢測,比如將”ABC”拆成”A”、”B”、”C”傳輸 。所以為了更好的提高snort的檢測性能,snort在進行檢測之前會將數據包進行重組,以防止分片攻擊的發生。
如果陸續有100個分片進入snort,就會出現以下情況:1、100個分片中有多個數據包。2、每個數據包有多個分片。
為了清楚的分出屬于同一IP包的不同分片,需要采用至少二維的結構。snort采用了二級二叉樹。主二叉樹用于記錄數據包的特征,然后二級二叉樹用于記錄這個數據包下的所有分片包。主二叉樹每一個節點都指向一個FragTracker結構,結構如下所示:
typedef struct _FragTracker
{
ubi_trNode Node; /*二叉樹結點*/
u_int32_t sip; /* src IP */
u_int32_t dip; /* dst IP */
u_int16_t id; /* IP ID */
u_int8_t protocol; /* IP protocol */
u_int8_t ttl; /* ttl used to detect evasions */
u_int8_t alerted;
u_int32_t frag_flags;
u_int32_t last_frag_time;
u_int32_t frag_bytes;
u_int32_t calculated_size;
u_int32_t frag_pkts;
ubi_trRoot fraglist; /*二級二叉樹根節點*/
ubi_trRootPtr fraglistPtr; /*指向二級二叉樹根節點的指針*/
} FragTracker;
Frag2會對新來的分片包在主二叉樹中進行匹配,如果找到了匹配的節點,那么這個分片包就是這個節點對應數據包的后續分片包,就會被加入到該節點的二級二叉樹中。如果不匹配,那說明這個分片包來自一個全新的數據包,就會給這個分片包分配一個全新的主二叉樹節點,并把這個分片包作為第一個分片加入。同時程序會判斷二級二叉樹是否已經接收所有的分片包,如果是則會對數據包進行重組。
首先是初始化,這個過程和上面說的一樣,這里就不重復了。
FragTracker *GetFragTracker(Packet *p)
{
FragTracker ft;
FragTracker *returned;
if(ubi_trCount(FragRootPtr) == 0)
return NULL;
if(f2data.frag_sp_data.mem_usage == 0)
{
DEBUG_WRAP(DebugMessage(DEBUG_FRAG2, "trCount says nodes exist but "
"f2data.frag_sp_data.mem_usage = 0\n"););
return NULL;
}
ft.sip = p->iph->ip_src.s_addr;
ft.dip = p->iph->ip_dst.s_addr;
ft.id = p->iph->ip_id;
ft.protocol = p->iph->ip_proto;
returned = (FragTracker *) ubi_sptFind(FragRootPtr, (ubi_btItemPtr)&ft);
DEBUG_WRAP(DebugMessage(DEBUG_FRAG2, "returning %p\n", returned););
return returned;
}
檢測分片包的特征(IP地址、上層協議、ID值等),在主二叉樹中是否存在,存在則返回fragtracker變量 。這個函數其實是通過ubi_sptFind函數實現的,而ubi_sptFind則是通過ubi_btFind函數實現的,ubi_btFind又調用了qFind函數實現,如下為qFind函數的源碼:
static ubi_btNodePtr qFind( ubi_btCompFunc cmp,
ubi_btItemPtr FindMe,
register ubi_btNodePtr p )
/* ------------------------------------------------------------------------ **
* This function performs a non-recursive search of a tree for a node
* matching a specific key. It is called "qFind()" because it is
* faster that TreeFind (below).
*
* Input:
* cmp - a pointer to the tree's comparison function.
* FindMe - a pointer to the key value for which to search.
* p - a pointer to the starting point of the search. <p>
* is considered to be the root of a subtree, and only
* the subtree will be searched.
*
* Output:
* A pointer to a node with a key that matches the key indicated by
* FindMe, or NULL if no such node was found.
*
* Note: In a tree that allows duplicates, the pointer returned *might
* not* point to the (sequentially) first occurance of the
* desired key.
* ------------------------------------------------------------------------ **
*/
{
int tmp;
while( (NULL != p)
&& ((tmp = ubi_trAbNormal( (*cmp)(FindMe, p) )) != ubi_trEQUAL) )
p = p->Link[tmp];
return( p );
} /* qFind */
可以看到qFind先是通過while循環遍歷二叉樹,而p->link是指向下一個節點的,但是到底是指向左節點、右節點還是父節點是通過tmp的值傳遞的,而tmp的值是通過cmp函數指針獲得的,cmp函數指針指向的是一個CompareFunc函數,通過這個函數判斷節點是否匹配,返回相應的值,然后這個返回的值會被傳到ubi_trAbNormal中,這是一個宏定義,源碼如下:
#define ubi_trAbNormal(W) ((char)( ((char)ubi_btSgn( (long)(W) )) \
+ ubi_trEQUAL ))
里面又使用了ubi_btSgn函數,源碼如下:
long ubi_btSgn( register long x )
/* ------------------------------------------------------------------------ **
* Return the sign of x; {negative,zero,positive} ==> {-1, 0, 1}.
*
* Input: x - a signed long integer value.
*
* Output: the "sign" of x, represented as follows:
* -1 == negative
* 0 == zero (no sign)
* 1 == positive
*
* Note: This utility is provided in order to facilitate the conversion
* of C comparison function return values into BinTree direction
* values: {LEFT, PARENT, EQUAL}. It is INCORPORATED into the
* ubi_trAbNormal() conversion macro!
*
* ------------------------------------------------------------------------ **
*/
{
return( (x)?((x>0)?(1):(-1)):(0) );
} /* ubi_btSgn */
正如注釋所說-1、1、0代表二叉樹的不同節點,然后tmp根據宏處理之后的結果,決定主二叉樹下一個是哪個節點。
如果在主二叉樹中沒有找到匹配的,說明這個分片包是一個全新的數據包,就會調用這個函數在主二叉樹上創建一個全新的節點。
該函數會先調用SPAlloc函數用于在主二叉樹上分配一個二叉樹節點,并將這個數據包的地址、協議、ID等特征信息賦給Frag2Tracker結構體中對應的變量。然后會調用ubi_trInitTree函數,這個函數會 初始化ubi_btRoot結構,這個結構里有RootPtr->cmp=CompFunc,在GetFragTracker里應該會用到 。
當主二叉樹的節點建立完之后,需要將分片包插入到對應的二級二叉樹中,這時就需要使用InsertFrag()函數。
InsertFrag()函數首先調用SPAlloc()函數申請一個新的節點,然后會使用ubi_sptFind()函數查找二級二叉樹中當前的分片包是否已經存在,如果不存在就會調用ubi_sptInsert()函數將其插入二級二叉樹中,而ubi_sptInsert()函數實際上是通過ubi_btInsert()函數實際執行的,聲明源碼如下:
ubi_trBool ubi_btInsert( ubi_btRootPtr RootPtr,
ubi_btNodePtr NewNode,
ubi_btItemPtr ItemPtr,
ubi_btNodePtr *OldNode );
部分重要源碼:
/* Find a place for the new node. */
*OldNode = TreeFind(ItemPtr, (RootPtr->root), &parent, &tmp, (RootPtr->cmp));
/* Now add the node to the tree... */
if( NULL == (*OldNode) ) /* The easy one: we have a space for a new node! */
{
if( NULL == parent )
RootPtr->root = NewNode; /*作為根節點插入*/
else
{
parent->Link[(int)tmp] = NewNode; /*插入新節點*/
NewNode->Link[ubi_trPARENT] = parent; /*指回父節點*/
NewNode->gender = tmp;
}
(RootPtr->count)++;
return( ubi_trTRUE );
}
通過TreeFind()函數遍歷二叉樹找到找到下一節點為空的節點,然后將新的節點插入。TreeFind()函數的實現和qFind()函數基本一致,就不重復了。
FragIsComplicate()函數和RebuildFrag()函數
FragIsComplicate()函數主要是用來檢查二級二叉樹是不是已經接收了所有的分片包。這個函數過程中最重要的就是對二叉樹的遍歷,而這個遍歷過程的函數調用流程可以從以上的樹狀圖中可以看出。先是調用了ubi_trTraverse,這是一個宏定義,源碼如下:
#define ubi_trTraverse( Rp, En, Ud ) \
ubi_btTraverse((ubi_btRootPtr)(Rp), (ubi_btActionRtn)(En), (void *)(Ud))
本質上是調用了ubi_btTraverse()函數,源碼如下:
unsigned long ubi_btTraverse( ubi_btRootPtr RootPtr,
ubi_btActionRtn EachNode,
void *UserData )
/* ------------------------------------------------------------------------ **
* Traverse a tree in sorted order (non-recursively). At each node, call
* (*EachNode)(), passing a pointer to the current node, and UserData as the
* second parameter.
*
* Input: RootPtr - a pointer to an ubi_btRoot structure that indicates
* the tree to be traversed.
* EachNode - a pointer to a function to be called at each node
* as the node is visited.
* UserData - a generic pointer that may point to anything that
* you choose.
*
* Output: A count of the number of nodes visited. This will be zero
* if the tree is empty.
*
* ------------------------------------------------------------------------ **
*/
{
ubi_btNodePtr p = ubi_btFirst( RootPtr->root );
unsigned long count = 0;
while( NULL != p )
{
(*EachNode)( p, UserData );
count++;
if(RootPtr->count != 0)
p = ubi_btNext( p );
else
return count;
}
return( count );
} /* ubi_btTraverse */
(*EachNode)( p, UserData );這句是關鍵,但是直接看這個函數中的這句會發現中間的這些參數都是形參,不方便理解,所以我們找到FragIsComplicate()函數中傳入的參數。
(void)ubi_trTraverse(ft->fraglistPtr, CompletionTraverse, compdata);
(*EachNode)是一個函數指針,而我們傳入的函數時CompletionTraverse()函數,所以這句話相當于時調用了CompletionTraverse()函數來檢驗每個分片包的偏移量,來判斷所有分片是否已經完整。
CompletionTraverse()函數源碼如下:
static void CompletionTraverse(ubi_trNodePtr NodePtr, void *complete)
{
Frag2Frag *frag;
CompletionData *comp = (CompletionData *) complete;
frag = (Frag2Frag *) NodePtr;
if(frag->offset == next_offset) /*next_offset全局變量用于記錄當前偏移地址值*/
{
next_offset = frag->offset + frag->size; /*更新next_offset*/
}
else if(frag->offset < next_offset) /*當前offset和之前的offset重疊,發生teardrop攻擊*/
{
/* flag a teardrop attack detection */
comp->teardrop = 1;
if(frag->size + frag->offset > next_offset)
{
next_offset = frag->offset + frag->size;
}
}
else if(frag->offset > next_offset) /*當前分片與前一片存在空白,分片數據還不完整*/
{
DEBUG_WRAP(DebugMessage(DEBUG_FRAG2, "Holes in completion check... (%u > %u)\n",
frag->offset, next_offset););
comp->complete = 0; /*不完整不能重組*/
}
return;
}
當Frag2IsComplicate()所有的分片包都已經收集的時候,就會調用RebuildFrag()函數對數據包進行重組。
六、ARP欺騙檢測插件——ARPspoof插件分析
插件大體的流程和Frag2插件流程類似,就整體粗略的分析一下,可以按照上述Frag2插件類似的分析方法對該插件進行分析。
也是有一個Setup函數用于該插件的初始化,里面又分成兩個部分,Init和HostInit函數,Init函數就是常規的初始化,而HostInit函數則是用于讀取snort.conf配置文件中的MAC-IP對照表。
然后這個插件的主功能函數模塊時DetectARPattacks()函數,這里借用《Snort入侵檢測系統源碼分析—獨孤九賤》書中的流程圖解釋:
參考資料:
《Snort入侵檢測系統源碼分析—獨孤九賤》