學習一下新東西Snort作為一個輕量級的網路入侵偵測系統,在實際中應用可能會有些力不從心,但如果想瞭解研究IDS的工作原理,仔細研究一下它的源碼到是非常不錯.首先對snort做一個概括的評論。
從工作原理而言,snort是一個NIDS。[註:基於網路的入侵偵測系統(NIDS)在網路的一點被動地檢查原始的網路傳輸資料。通過分析檢查的資料包,NIDS匹配入侵行為的特徵或者從網路活動的角度檢測異常行為。] 網路傳輸資料的採集利用了工具包libpcap。snort對libpcap採集來的資料進行分析,從而判斷是否存在可疑的網路活動。
從檢測模式而言,snort基本上是誤用檢測(misuse detection)。[註:該方法對已知攻擊的特徵模式進行匹配,包括利用工作在網卡混雜模式下的嗅探器被動地進行協議分析,以及對一系列資料包解釋分析特徵。順便說一句,另一種檢測是異常檢測(anomaly detection)。]具體實現上,僅僅是對資料進行最直接最簡單的搜尋匹配,並沒有涉及更複雜的入侵檢測辦法。
儘管snort在實現上沒有什麼高深的檢測策略,但是它給我們提供了一個非常
優秀的公開原始碼的入侵偵測系統範例。我們可以通過對其代碼的分析,搞清IDS 究竟是如何工作的,並在此基礎上添加自己的想法。
snort的編程風格非常優秀,代碼閱讀起來並不困難,整個程式結構清晰,函
數調用關係也不算複雜。但是,snort的源檔案不少,函數總數也很多,所以不太
容易講清楚。因此,最好把代碼完整看一兩遍,能更清楚點。
*****************************************************
*****************************************************
下面看看snort的整體結構。展開snort壓縮包,有約50個c程式和標頭檔,另有約30個其它檔案(工程、資料或者說明檔案)。[註:這裡用的是snort-1.6-beta7。snort-1.6.3不在手邊,就用老一點的版本了,差別不大。]下面對原始碼檔案分組說明。
snort.c(.h)是主程式所在的檔案,實現了main函數和一系列輔助函數。
decode.c(.h)把資料包層層剝開,確定該包屬於何種協議,有什麼特徵。並
標記到全域結構變數pv中。
log.c(.h)實現日誌和警示功能。snort有多種日誌格式,一種是按tcpdump二進位的格式儲存,另一種按snort編碼的ascii格式儲存在日誌目錄下,日誌目錄的名字根據"外"主機的ip地址命名。警示有不同的層級和方式,可以記錄到syslog中,或者記錄到使用者指定的檔案,另外還可以通過unix socket發送警示訊息,以及利用SMB向Windows系統發送winpopup訊息。
mstring.c(.h)實現字串匹配演算法。在snort中,採用的是Boyer-Moore演算法。演算法書上一般都有。
plugbase.c(.h)實現了初始化檢測以及登記檢測規則的一組函數。snort中的檢測規則以鏈表的形式儲存,每條規則通過登記(Register)過程添加到鏈表中。
response.c(.h)進行響應,即向攻擊方主動發送資料包。這裡實現了兩種響應。一種是發送ICMP的主機不可到達的假資訊,另一種針對TCP,發送RST包,中斷連線。
rule.c(.h)實現了規則設定和入侵檢測所需要的函數。規則設定主要的作用是
把一個規則檔案轉化為實際運作中的規則鏈表。檢測函數根據規則實施攻擊特徵的檢測。
sp_*_check.c(.h)是不同類型的檢測規則的具體實現。很容易就可以從檔案名稱得知所實現的規則。例如,sp_dsize_check針對的是包的資料大小,sp_icmp_type_check針對icmp包的類型,sp_tcp_flag_check針對tcp包的標誌位。不再詳述。
spo_*.c(.h)實現輸出(output)規則。spo_alert_syslog把事件記錄到syslog中;spo_log_tcpdump利用libpcap中的日誌函數,進行日誌記錄。
spp_*.c(.h)實現預先處理(preprocess)規則。包括http解碼(即把http請求中的%XX這樣的字元用對應的ascii字元代替,避免忽略了惡意的請求)、最小片斷檢查(避免惡意利用tcp協議中重組的功能)和連接埠掃描檢測。
********************************************************************************************************** 下面描述main函數的工作流程。先來說明兩個結構的定義。
在snort.h中,定義了兩個結構:PV和PacketCount。PV用來記錄命令列參數,snort根據這些命令列參數來確定其工作方式。PV類型的全域變數pv用來實際記錄具體工作方式。結構定義可以參看snort.h,在下邊的main函數中,會多次遇到pv中各個域的設定,到時再一個一個解釋。
結構PacketCount用來統計流量,每處理一個資料包,該結構類型的全域變數pc把對應的域加1。相當於一個計數器。
接下來解釋main函數。
初始化設定一些預設值;然後解析命令列參數,根據命令列參數,填充結構變數pv;根據pv的值(也就是解析命令列的結果)確定工作方式,需要注意:
如果是運行在Daemon方式,通過GoDaemon函數,建立守護進程,重新導向標準輸入輸出,實現daamon狀態,並結束父進程。
snort可以即時採集網路資料,也可以從檔案讀取資料進行分析。這兩種情況並沒有本質區別。如果是讀取檔案進行分析(並非直接從網卡即時採集來的),以該檔案名稱作為libpcap的函數OpenPcap的參數,開啟採集過程;如果是從網卡即時採集,就把網卡介面作為OpenPcap的參數,利用libpcap的函數開啟該網卡介面。在unix中,裝置也被看作是檔案,所以這和讀取檔案分析沒有多大的差別。
接著,指定資料包的拆包函數。不同的資料鏈路網路,拆包的函數也不同。利用函數SetPktProcessor,根據全域變數datalink的值,來設定不同的拆包函數。例如,乙太網路,拆包函數為DecodeEthPkt;令牌環網,拆包函數為DecodeTRPkt,等等。這些Decode*函數,在decode.c中實現。
如果使用了檢測規則,那麼下面就要初始化這些檢測規則,並解析規則檔案,轉化成規則鏈表。規則有三大類:預先處理(preprocessor),外掛程式(plugin),輸出外掛程式(outputplugin)。這裡plugin就是具體的檢測規則,而outputplugin是定義日誌和警示方式的規則。
然後根據警示模式,設定警示函數;根據記錄模式,設定日誌函數;如果指定了能夠進行響應,就開啟raw socket,準備用於響應。
最後進入讀取資料包的迴圈,pcap_loop對每個採集來的資料包都用ProcessPacket函數進行處理,如果出現錯誤或者到達指定的處理包數(pv.pkt_cnt定義),就退出該函數。這裡ProcessPacket是關鍵程式,
最後,關閉採集過程。
*****************************************************
現在看看snort如何?對資料包的分析和檢測入侵的。
在main函數的最後部分有如下語句,比較重要:
/* Read all packets on the device. Continue until cnt packets read */
if(pcap_loop(pd, pv.pkt_cnt, (pcap_handler)ProcessPacket, NULL) < 0)
{
......
}
這裡pcap_loop函數有4個參數,分別解釋:
pd是一個全域變數,表示檔案描述符,在前面OpenPcap的調用中已經被正確地賦值。前面說過,snort可以即時採集網路資料,也可以從檔案讀取資料進行分析。在不同情況開啟檔案(或裝置)時,pd分別用來處理檔案,或者網卡裝置介面。
pd是struct pcap類型的指標,該結構包括實際的檔案描述符,緩衝區,等等域,用來處理從相應的檔案擷取資訊。
OpenPcap函數中對pd賦值的語句分別為:
/* get the device file descriptor,開啟網卡介面 */
pd = pcap_open_live(pv.interface, snaplen,
pv.promisc_flag ? PROMISC : 0, READ_TIMEOUT, errorbuf);
或者
/* open the file,開啟檔案 */
pd = pcap_open_offline(intf, errorbuf);
於是,這個參數表明從哪裡取得待分析的資料。
第2個參數是pv.pkt_cnt,表示總共要捕捉的包的數量。在main函數初始化時,預設設定為-1,成為永真迴圈,一直捕捉直到程式退出:
/* initialize the packet counter to loop forever */
pv.pkt_cnt = -1;
或者在命令列中設定要捕捉的包的數量。前面ParseCmdLine(解析命令列)函數的調用中,遇到參數n,重新設定pv.pkt_cnt的值。ParseCmdLine中相關語句如下:
case 'n': /* grab x packets and exit */
pv.pkt_cnt = atoi(optarg);
第3個參數是回呼函數,該回呼函數處理捕捉到的資料包。這裡為函數
ProcessPacket,下面將詳細解釋該函數。
第4個參數是字串指標,表示使用者,這裡設定為空白。
在說明處理包的函數ProcessPacket之前,有必要解釋一下pcap_loop的實現。我們看到main函數只在if條件判斷中調用了一次pacp_loop,那麼迴圈一定是在pcap_loop中做的了。察看pcap.c檔案中pcap_loop的實現部分,我們發現的確如此:
int
pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
{
register int n;
for (; { //for迴圈
if (p->sf.rfile != NULL)
n = pcap_offline_read(p, cnt, callback, user);
else {
/*
* XXX keep reading until we get something
* (or an error occurs)
*/
do { //do迴圈
n = pcap_read(p, cnt, callback, user);
} while (n == 0);
}
if (n <= 0)
return (n); //遇到錯誤,返回
if (cnt > 0) {
cnt -= n;
if (cnt <= 0)
return (0); //到達指定數量,返回
}
//只有以上兩種返回情況
}
}
現在看看ProcessPacket的實現了,這個回呼函數用來處理資料包。該函數是是pcap_handler類型的,pcap.h中類型的定義如下:
typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
const u_char *);
第1個參數這裡沒有什麼用;
第2個參數為pcap_pkthdr結構指標,記錄時間戳記、包長、捕捉的長度;
第3個參數字串指標為資料包。
函數如下:
void ProcessPacket(char *user, struct pcap_pkthdr *pkthdr, u_char *pkt)
{
Packet p; //Packet結構在decode.h中定義,用來記錄資料包的各種資訊
/* call the packet decoder,調用拆包函數,這裡grinder是一個全域
函數指標,已經在main的SetPktProcessor調用中設定為正確的拆包函數 */
(*grinder)(&p, pkthdr, pkt);
/* print the packet to the screen,如果選擇了詳細顯示方式,
那麼把包的資料,顯示到標準輸出 */
if(pv.verbose_flag)
{
...... //省略
}
/* check or log the packet as necessary
如果工作在使用檢測規則的方式,就調用Preprocess進行檢測,
否則,僅僅進行日誌,記錄該包的資訊*/
if(!pv.use_rules)
{
... //進行日誌,省略
}
else
{
Preprocess(&p);
}
//清除緩衝區
ClearDumpBuf();
}
這裡Preprocess函數進行實際檢測。
****************************************************************************
Proprocess函數很短,首先調用預先處理規則處理資料包p,然後調用檢測
函數Detect進行規則匹配實現檢測,如果實現匹配,那麼調用函數CallOutput
Plugins根據輸出規則進行警示或日誌。函數如下:
void Preprocess(Packet *p)
{
PreprocessFuncNode *idx;
do_detect = 1;
idx = PreprocessList; //指向預先處理規則鏈表頭
while(idx != NULL) //調用預先處理函數處理包p
{
idx->func(p);
idx = idx->next;
}
if(!p->frag_flag && do_detect)
{
if(Detect(p)) //調用檢測函數
{
CallOutputPlugins(p); //如果匹配,根據規則輸出
}
}
} 儘管這個函數很簡潔,但是在第1行我們看到定義了ProprocessFuncNode
結構類型的指標,所以下面,我們不得不開始涉及到snort的各種複雜
的資料結構。前面的分析,我一直按照程式啟動並執行調用順序,忽略了許多函
數(其實有不少非常重要),以期描述出snort執行的主線,避免因為程式中
大量的調用關係而產生混亂。到現在,我們還沒有接觸到snort核心的資料結構
和演算法。有不少關鍵的問題需要解決:規則是如何靜態描述的?運行時這些
規則按照什麼結構動態儲存裝置?每條規則的處理函數如何被調用?snort給了
我們提供了非常好的方法。
snort一個非常成功的思想是利用了plugin機制,規則處理函數並非固定在
來源程式中,而是根據每次運行時的參數設定,從規則檔案中讀入規則,再把每個
規則所需要的處理函數掛接到鏈表上。實際檢測時,遍曆這些鏈表,調用鏈表上
相應的函數來分析。
snort主要的資料結構是鏈表,幾乎都是鏈表來鏈表去。我們下面做個總的
介紹。
我們有必要先回過頭來,看一看main函數中對規則初始化時涉及到的一些
資料結構。
在main函數初始化規則的時候,先建立了幾個鏈表,全域變數定義如下
(plugbase.c中):
KeywordXlateList *KeywordList;
PreprocessKeywordList *PreprocessKeywords;
PreprocessFuncNode *PreprocessList;
OutputKeywordList *OutputKeywords;
OutputFuncNode *OutputList;
這幾種結構的具體定義省略。這一初始化的過程把snort中預定義的關鍵
字和處理函數按類別串連在不同的鏈表上。然後,在解析規則檔案的時候,
如果一條規則的選項中包含了某個關鍵字,就會從上邊初始化好的對應的鏈表
中尋找,把必要的資訊和處理函數添加到表示這條規則的節點(用RuleTreeNode
類型來表示,下面詳述)的特定域(OptTreeNode類型)中。
同時,main函數中初始化規則的最後,對指定的規則檔案進行解析。在最
高的層次上,有3個全域變數儲存規則(rules.c):
ListHead Alert; /* Alert Block Header */
ListHead Log; /* Log Block Header */
ListHead Pass; /* Pass Block Header */
這幾個變數是ListHead類型的,正如名稱所說,指示鏈表頭。Alert中登記
了需要警示的規則,Log中登記了需要進行日誌的規則,Pass中登記的規則在處
理過程忽略(不進行任何處理)。ListHead定義如下:
typedef struct _ListHead
{
RuleTreeNode *TcpList;
RuleTreeNode *UdpList;
RuleTreeNode *IcmpList;
} ListHead;
可以看到,每個ListHead結構中有三個指標,分別指向處理Tcp/Udp/Icmp包規則的鏈表頭。這裡又出現了新的結構RuleTreeNode,為了說明鏈表的層次關係,下面列出RuleTreeNode的定義,但是忽略了大部分域:
typedef struct _RuleTreeNode
{
RuleFpList *rule_func;
...... //忽略
struct _RuleTreeNode *right;
OptTreeNode *down; /* list of rule options to associate with this
rule node */
} RuleTreeNode;
RuleTreeNode中包含上述3個指標域,分別又能形成3個鏈表。RuleTreeNode*類型的right指向下一個RuleTreeNode,相當於普通鏈表中的next域,只不過這裡用right來命名。這樣就形成了規則鏈表。
RuleFpList類的指標rule_func記錄的是該規則的處理函數的鏈表。一條規則有時候需要調用多個處理函數來分析。所以,有必要做成鏈表。我們看看下面的定義,除了next域,還有一個函數指標:
typedef struct _RuleFpList
{
/* rule check function pointer */
int (*RuleHeadFunc)(Packet *, struct _RuleTreeNode *, struct _RuleFpList *);
/* pointer to the next rule function node */
struct _RuleFpList *next;
} RuleFpList;
第3個指標域是OptTreeNode類的指標down,該行後面的注釋說的很清楚,這是與這個規則節點相聯絡的規則選項的鏈表。很不幸,OptTreeNode的結構也相當複雜,而且又引出了幾個新的鏈表。忽略一些域,OptTreeNode定義如下:
typedef struct _OptTreeNode
{
/* plugin/detection functions go here */
OptFpList *opt_func;
/* the ds_list is absolutely essential for the plugin system to work,
it allows the plugin authors to associate "dynamic" data structures
with the rule system, letting them link anything they can come up
with to the rules list */
void *ds_list[512]; /* list of plugin data struct pointers */
.......//省略了一些域
struct _OptTreeNode *next;
} OptTreeNode;
next指向鏈表的下一個節點,無需多說。OptFpList類型的指標opt_func指向
選項函數鏈表,同前面說的RuleFpList沒什麼大差別。值得注意的是指標數組
ds_list,用來記錄該條規則中涉及到的預定義處理過程。每個元素的類型是void*.在實際表示規則的時候,ds_list被強制轉換成不同的預定義類型。
--------------------------------------------------------------------------------------
Proprocess函數很短,首先調用預先處理規則處理資料包p,然後調用檢測
函數Detect進行規則匹配實現檢測,如果實現匹配,那麼調用函數CallOutput
Plugins根據輸出規則進行警示或日誌。函數如下:
void Preprocess(Packet *p)
{
PreprocessFuncNode *idx;
do_detect = 1;
idx = PreprocessList; //指向預先處理規則鏈表頭
while(idx != NULL) //調用預先處理函數處理包p
{
idx->func(p);
idx = idx->next;
}
if(!p->frag_flag && do_detect)
{
if(Detect(p)) //調用檢測函數
{
CallOutputPlugins(p); //如果匹配,根據規則輸出
}
}
}
儘管這個函數很簡潔,但是在第1行我們看到定義了ProprocessFuncNode
結構類型的指標,所以下面,我們不得不開始涉及到snort的各種複雜
的資料結構。前面的分析,我一直按照程式啟動並執行調用順序,忽略了許多函
數(其實有不少非常重要),以期描述出snort執行的主線,避免因為程式中
大量的調用關係而產生混亂。到現在,我們還沒有接觸到snort核心的資料結構
和演算法。有不少關鍵的問題需要解決:規則是如何靜態描述的?運行時這些
規則按照什麼結構動態儲存裝置?每條規則的處理函數如何被調用?snort給了
我們提供了非常好的方法。
snort一個非常成功的思想是利用了plugin機制,規則處理函數並非固定在
來源程式中,而是根據每次運行時的參數設定,從規則檔案中讀入規則,再把每個
規則所需要的處理函數掛接到鏈表上。實際檢測時,遍曆這些鏈表,調用鏈表上
相應的函數來分析。
snort主要的資料結構是鏈表,幾乎都是鏈表來鏈表去。我們下面做個總的
介紹。
我們有必要先回過頭來,看一看main函數中對規則初始化時涉及到的一些
資料結構。
在main函數初始化規則的時候,先建立了幾個鏈表,全域變數定義如下
(plugbase.c中):
KeywordXlateList *KeywordList;
PreprocessKeywordList *PreprocessKeywords;
PreprocessFuncNode *PreprocessList;
OutputKeywordList *OutputKeywords;
OutputFuncNode *OutputList;
這幾種結構的具體定義省略。這一初始化的過程把snort中預定義的關鍵
字和處理函數按類別串連在不同的鏈表上。然後,在解析規則檔案的時候,
如果一條規則的選項中包含了某個關鍵字,就會從上邊初始化好的對應的鏈表
中尋找,把必要的資訊和處理函數添加到表示這條規則的節點(用RuleTreeNode
類型來表示,下面詳述)的特定域(OptTreeNode類型)中。
同時,main函數中初始化規則的最後,對指定的規則檔案進行解析。在最
高的層次上,有3個全域變數儲存規則(rules.c):
ListHead Alert; /* Alert Block Header */
ListHead Log; /* Log Block Header */
ListHead Pass; /* Pass Block Header */
這幾個變數是ListHead類型的,正如名稱所說,指示鏈表頭。Alert中登記
了需要警示的規則,Log中登記了需要進行日誌的規則,Pass中登記的規則在處
理過程忽略(不進行任何處理)。ListHead定義如下:
typedef struct _ListHead
{
RuleTreeNode *TcpList;
RuleTreeNode *UdpList;
RuleTreeNode *IcmpList;
} ListHead;
可以看到,每個ListHead結構中有三個指標,分別指向處理Tcp/Udp/Icmp包規則的鏈表頭。這裡又出現了新的結構RuleTreeNode,為了說明鏈表的層次關係,下面列出RuleTreeNode的定義,但是忽略了大部分域:
typedef struct _RuleTreeNode
{
RuleFpList *rule_func;
...... //忽略
struct _RuleTreeNode *right;
OptTreeNode *down; /* list of rule options to associate with this
rule node */
} RuleTreeNode;
RuleTreeNode中包含上述3個指標域,分別又能形成3個鏈表。RuleTreeNode*
類型的right指向下一個RuleTreeNode,相當於普通鏈表中的next域,只不過這裡用right來命名。這樣就形成了規則鏈表。
RuleFpList類的指標rule_func記錄的是該規則的處理函數的鏈表。一條規則有時候需要調用多個處理函數來分析。所以,有必要做成鏈表。我們看看下面的
定義,除了next域,還有一個函數指標:
typedef struct _RuleFpList
{
/* rule check function pointer */
int (*RuleHeadFunc)(Packet *, struct _RuleTreeNode *, struct _RuleFpList *);
/* pointer to the next rule function node */
struct _RuleFpList *next;
} RuleFpList;
第3個指標域是OptTreeNode類的指標down,該行後面的注釋說的很清楚,這是與這個規則節點相聯絡的規則選項的鏈表。很不幸,OptTreeNode的結構也相當複雜,而且又引出了幾個新的鏈表。忽略一些域,OptTreeNode定義如下:
typedef struct _OptTreeNode
{
/* plugin/detection functions go here */
OptFpList *opt_func;
/* the ds_list is absolutely essential for the plugin system to work,
it allows the plugin authors to associate "dynamic" data structures
with the rule system, letting them link anything they can come up
with to the rules list */
void *ds_list[512]; /* list of plugin data struct pointers */
.......//省略了一些域
struct _OptTreeNode *next;
} OptTreeNode;
next指向鏈表的下一個節點,無需多說。OptFpList類型的指標opt_func指向選項函數鏈表,同前面說的RuleFpList沒什麼大差別。值得注意的是指標數組
ds_list,用來記錄該條規則中涉及到的預定義處理過程。每個元素的類型是void*。在實際表示規則的時候,ds_list被強制轉換成不同的預定義類型。 原文地址:http://www.yuanma.org/data/2006/0912/article_1515.htm