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被强制转换成不同的预定义类型。