SNORT源码分析说课讲解.docx
《SNORT源码分析说课讲解.docx》由会员分享,可在线阅读,更多相关《SNORT源码分析说课讲解.docx(18页珍藏版)》请在冰豆网上搜索。
SNORT源码分析说课讲解
SNORT源码分析
SNORT源码分析(转自SMTH)
Snort作为一个轻量级的网络入侵检测系统,在实际中应用可能会有些力不从心,但如果想了解研究IDS的工作原理,仔细研究一下它的源码到是非常不错.首先对snort做一个概括的评论。
从工作原理而言,snort是一个NIDS。
[注:
基于网络的入侵检测系统(NIDS)在网络的一点被动地检查原始的网络传输数据。
通过分析检查的数据包,NIDS匹配入侵行为的特征或者从网络活动的角度检测异常行为。
]网络传输数据的采集利用了工具包libpcap。
snort对libpcap采集来的数据进行分析,从而判断是否存在可疑的网络活动。
从检测模式而言,snort基本上是误用检测(misusedetection)。
[注:
该方法对已知攻击的特征模式进行匹配,包括利用工作在网卡混杂模式下的嗅探器被动地进行协议分析,以及对一系列数据包解释分析特征。
顺便说一句,另一种检测是异常检测(anomalydetection)。
]具体实现上,仅仅是对数据进行最直接最简单的搜索匹配,并没有涉及更复杂的入侵检测办法。
尽管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中,或者记录到用户指定的文件,另外还可以通过unixsocket发送报警消息,以及利用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是定义日志和报警方式的规则。
然后根据报警模式,设定报警函数;根据日志模式,设定日志函数;如果指定了能够进行响应,就打开rawsocket,准备用于响应。
最后进入读取数据包的循环,pcap_loop对每个采集来的数据包都用ProcessPacket函数进行处理,如果出现错误或者到达指定的处理包数(pv.pkt_cnt定义),就退出该函数。
这里ProcessPacket是关键程序,
最后,关闭采集过程。
*****************************************************
现在看看snort如何实现对数据包的分析和检测入侵的。
在main函数的最后部分有如下语句,比较重要:
/*Readallpacketsonthedevice.Continueuntilcntpacketsread*/
if(pcap_loop(pd,pv.pkt_cnt,(pcap_handler)ProcessPacket,NULL)<0)
{
......
}
这里pcap_loop函数有4个参数,分别解释:
pd是一个全局变量,表示文件描述符,在前面OpenPcap的调用中已经被正确地赋值。
前面说过,snort可以实时采集网络数据,也可以从文件读取数据进行分析。
在不同情况打开文件(或设备)时,pd分别用来处理文件,或者网卡设备接口。
pd是structpcap类型的指针,该结构包括实际的文件描述符,缓冲区,等等域,用来处理从相应的文件获取信息。
OpenPcap函数中对pd赋值的语句分别为:
/*getthedevicefiledescriptor,打开网卡接口*/
pd=pcap_open_live(pv.interface,snaplen,
pv.promisc_flag?
PROMISC:
0,READ_TIMEOUT,errorbuf);
或者
/*openthefile,打开文件*/
pd=pcap_open_offline(intf,errorbuf);
于是,这个参数表明从哪里取得待分析的数据。
第2个参数是pv.pkt_cnt,表示总共要捕捉的包的数量。
在main函数初始化时,缺省设置为-1,成为永真循环,一直捕捉直到程序退出:
/*initializethepacketcountertoloopforever*/
pv.pkt_cnt=-1;
或者在命令行中设置要捕捉的包的数量。
前面ParseCmdLine(解析命令行)函数的调用中,遇到参数n,重新设定pv.pkt_cnt的值。
ParseCmdLine中相关语句如下:
case'n':
/*grabxpacketsandexit*/
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,intcnt,pcap_handlercallback,u_char*user)
{
registerintn;
for(;{//for循环
if(p->sf.rfile!
=NULL)
n=pcap_offline_read(p,cnt,callback,user);
else{
/*
*XXXkeepreadinguntilwegetsomething
*(oranerroroccurs)
*/
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中类型的定义如下:
typedefvoid(*pcap_handler)(u_char*,conststructpcap_pkthdr*,
constu_char*);
第1个参数这里没有什么用;
第2个参数为pcap_pkthdr结构指针,记录时间戳、包长、捕捉的长度;
第3个参数字符串指针为数据包。
函数如下:
voidProcessPacket(char*user,structpcap_pkthdr*pkthdr,u_char*pkt)
{
Packetp;//Packet结构在decode.h中定义,用来记录数据包的各种信息
/*callthepacketdecoder,调用拆包函数,这里grinder是一个全局
函数指针,已经在main的SetPktProcessor调用中设置为正确的拆包函数*/
(*grinder)(&p,pkthdr,pkt);
/*printthepackettothescreen,如果选择了详细显示方式,
那么把包的数据,显示到标准输出*/
if(pv.verbose_flag)
{
......//省略
}
/*checkorlogthepacketasnecessary
如果工作在使用检测规则的方式,就调用Preprocess进行检测,
否则,仅仅进行日志,记录该包的信息*/
if(!
pv.use_rules)
{
...//进行日志,省略
}
else
{
Preprocess(&p);
}
//清除缓冲区
ClearDumpBuf();
}
这里Preprocess函数进行实际检测。
****************************************************************************
Proprocess函数很短,首先调用预处理规则处理数据包p,然后调用检测
函数Detect进行规则匹配实现检测,如果实现匹配,那么调用函数CallOutput
Plugins根据输出规则进行报警或日志。
函数如下:
voidPreprocess(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):
ListHeadAlert;/*AlertBlockHeader*/
ListHeadLog;/*LogBlockHeader*/
ListHeadPass;/*PassBlockHeader*/
这几个变量是ListHead类型的,正如名称所说,指示链表头。
Alert中登记
了需要报警的规则,Log中登记了需要进行日志的规则,Pass中登记的规则在处
理过程忽略(不进行任何处理)。
ListHead定义如下:
typedefstruct_ListHead
{
RuleTreeNode*TcpList;
RuleTreeNode*UdpList;
RuleTreeNode*IcmpList;
}ListHead;
可以看到,每个ListHead结构中有三个指针,分别指向处理Tcp/Udp/Icmp包规则的链表头。
这里又出现了新的结构RuleTreeNode,为了说明链表的层次关系,下面列出RuleTreeNode的定义,但是忽略了大部分域:
typedefstruct_RuleTreeNode
{
RuleFpList*rule_func;
......//忽略
struct_RuleTreeNode*right;
OptTreeNode*down;/*listofruleoptionstoassociatewiththis
rulenode*/
}RuleTreeNode;
RuleTreeNode中包含上述3个指针域,分别又能形成3个链表。
RuleTreeNode*类型的right指向下一个RuleTreeNode,相当于普通链表中的next域,只不过这里用right来命名。
这样就形成了规则链表。
RuleFpList类的指针rule_func记录的是该规则的处理函数的链表。
一条规则有时候需要调用多个处理函数来分析。
所以,有必要做成链表。
我们看看下面的定义,除了next域,还有一个函数指针:
typedefstruct_RuleFpList
{
/*rulecheckfunctionpointer*/
int(*RuleHeadFunc)(Packet*,struct_RuleTreeNode*,struct_RuleFpList*);
/*pointertothenextrulefunctionnode*/
struct_RuleFpList*next;
}RuleFpList;
第3个指针域是OptTreeNode类的指针down,该行后面的注释说的很清楚,这是与这个规则节点相联系的规则选项的链表。
很不幸,OptTreeNode的结构也相当复杂,而且又引出了几个新的链表。
忽略一些域,OptTreeNode定义如下:
typedefstruct_OptTreeNode
{
/*plugin/detectionfunctionsgohere*/
OptFpList*opt_func;
/*theds_listisabsolutelyessentialforthepluginsystemtowork,
itallowsthepluginauthorstoassociate"dynamic"datastructures
withtherulesystem,lettingthemlinkanythingtheycancomeup
withtotheruleslist*/
void*ds_list[512];/*listofplugindatastructpointers*/
.......//省略了一些域
struct_OptTreeNode*next;
}OptTreeNode;
next指向链表的下一个节点,无需多说。
OptFpList类型的指针opt_func指向
选项函数链表,同前面说的RuleFpList没什么大差别。
值得注意的是指针数组
ds_list,用来记录该条规则中涉及到的预定义处理过程。
每个元素的类型是void*.在实际表示规则的时候,ds_list被强制转换成不同的预定义类型。
--------------------------------------------------------------------------------------
Proprocess函数很短,首先调用预处理规则处理数据包p,然后调用检测
函数Detect进行规则匹配实现检测,如果实现匹配,那么调用函数CallOutput
Plugins根据输出规则进行报警或日志。
函数如下:
voidPreprocess(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机制,规则处理函数并非固定在
源程序中,而是根据