目的
对snort1.0,1.5及以上版本
进行分析,报告内容比较清晰,文字描述比较准确,插图合适恰当,章节安排合乎逻辑
实验平台
- 操作系统:windows 10。(以自身操作系统为准)
- 使用软件:
- 文档编写工具:Typora(markdown语法编辑器)
- 代码管理工具:使用 Git 进行代码管理
- 代码查看工具:Sourcetrail
由于才疏学浅,所以先去百度找了工作流程的原理来方便阅读,
Snort 工作流程分为以下四个主要部分:
1、包捕获/解码引擎:首先,利用libpcap从网卡捕获网络上的数据包,然后数据包经过解码引擎填入到链路层协议的包结构体中,以便对高层次的协议进行解码,如TCP和UDP端口。
2、预处理器插件:接着,数据包被送到各种各样的预处理器中,在检测引擎处理之前进行检查和操作。每个预处理器检查数据包是否应该注意、报警或者修改某些东西。
3、规则解析和检测引擎:然后,包被送到检测引擎。检测引擎通过各种规则文件中的不同选项来对每个包的特征和包信息进行单一、简单的检测。检测引擎插件对包提供额外的检测功能。规则中的每个关键字选项对应于检测引擎插件,能够提供不同的检测功能。
4、输出插件:Snort通过检测引擎、预处理器和解码引擎输出报警。
详细工作流程:
一、包捕获/解码引擎:
1、数据包捕获:
Snort通过两种机制来满足网络流量的需要:将网卡设置为混杂模式;利用libpcap从网卡捕获网络数据包。
网卡的默认工作方式是忽略所有不是自己的MAC地址为目的地址的流量。通过将网卡设置成混杂模式,可以监听网络中的所有流量。
数据包捕获函数库是一个独立的软件工具,数据包捕获库函数能直接从网卡获取数据包。Snort就是通过调用该库函数从网络设备上捕获数据包。它工作在OSI模型的数据链路层。在不同的平台上使用Snort系统,需要安装不同版本的Libpcap,比如在Linux和Unix系统下需要安装Libpcap,而在Windows系列系统下,就需要安装Winpcap。
2、包解码:
对已经到达网卡并且被lipcap库函数捕获的数据包,Snort需调用解码引擎对数据链路层的原始数据包进行解码。Snort能够识别以太网、802.11(无线局域网协议)、令牌环以及诸如IP、TCP和UDP等高层协议。Snort将捕获的数据包解析后,存储在内存中指针指向的数据结构中。
二、预处理器:
Snort卓越的速度源于简单的规则匹配,如果只对每个包进行数字和字符串的匹配,处理性能就能适应快速的高负载网络。这种检测系统的缺点是,如果攻击模式很常见,就会产生很多误报。如果模式过于特殊,又会产生漏报。造成这些缺陷的原因是特征语言的表达能力有限或IDS对协议的分析不够,一些IDS通过复杂的方法解决这一问题。它们有些使用协议异常检测来对不符合协议规范的包报警,有些则保持连接状态,并只对处于以连接TCP会话中的包报警。
Snort则通过预处理器来实现这些功能,Snort有多个预处理器,包括frag2,stream4,http_decode,Telnet_negotiation,portscan,rpc_decode等常用的预处理器。这些预处理器的主要功能为:1)包重组;2) 协议解码;3) 异常检测;。
1、包重组:
基于特征的检测将包数据和定义良好的模式进行匹配。但是不能对跨包的数据进行检测。通过frag2插件将分片重组到一个完整包中,可以确保攻击者不能用IP分片来躲避检测。通过stream4插件进行流重组,我们可以用单包特征机制在TCP会话中跨越多个包进行模式匹配。最后,通过stream4的状态维护功能,特征匹配可以具备一些智能,来判断哪些包应该丢弃,哪些包处于链接中。用一句话总结,包重组预处理器帮助Snort检测匹配数据分布在多个包中的攻击。
2、协议解码:
基于规则的检测提供简单的串/位匹配的功能,但无法检测HTTP协议中不同形式的URL,除非使用无限大的规则集。 http_de-code预处理器帮助Snort在规则匹配前规范化URL。简单的规则匹配还可能因为数据中间插入的协议信息而失败。Telnet_negoti-ation和rpc_decode预处理器去除数据中不应该进行模式匹配的部分,rpc_decode预处理合并RPC消息分片,Telnet_negotiation预处理器去除Telnet的协议协商过程。用一句话总结,协议解码预处理对协议数据进行处理,使串匹配功能在更明确的数据上工作。
3、异常检测:
基于规则的检测由于其简单性而工作可靠。它的过程精确,可以很容易地调整配置减少误报,也容易进行优化。但是,有些攻击无法通过这种模式来检测。Snort发展了协议异常检测:portscan预处理器允许Snort跟踪在一个时间段内接收的扫描类型的包,对超过阈值的情况进行报警;BackOrifice预处理器使Snort不用巨大的规则集就可以检测加密的BackOrifice流量。
三、规则解析和检测引擎:
Snort规则是基于文本的,它通常存在于Snort程序目录中或者子目录中。在启动的时候,Snort读取所有的规则文件,并且建立一个三维的链表。Snort使用列表匹配包和检测。
1、规则解析:
Snort规则是Snort入侵检测系统的重要组成部分。规则集是snort的攻击特库,每条规则都对应一条攻击特征,snort通过它来识别攻击行为。
每一条规包括两个部分:规则头部(RuleHeader)和规则选项(RuleOption)。规则头包含规则的行为、协议、源地址、目的地址、子网掩码、源和目的端口信息。规则选项包含报警信息以及规则触发时提供给管理员的参考信息。例如: alerttcpanyany->202.203.112.0/24any(content:“proxy-connection”;msg:“proxyuse”😉 这条规则述信息是:对任何访问202.203.112网段,tcp报文中含有proxy-connection的流量报警。在这个例子中,左括号前面是规则头。圆括号内的内容是规则选项,在规则选项中,content后面的内容为关键字,及需要匹配的字符串,msg后面的内容为规则触发时将显示的信息。Snort的规则定义中可以没有规则体,它们只是用来更好地定义所要进行的某种处理(记录、报警、忽略等)的数据包类型。只有当规则中的每一个元素都为真时,才能触发对应的规则动作。
2、检测引擎:
Snort把具有相同条件的规则链接到一个集合中,用RTN结构来描述;规则选项对应于规则选项结点OTN(OptionalTreeNode),包含一些特定的检测标志、报警信息、匹配内容等条件,每个选项的匹配子函数(插件)放到FUNC链表中。只有当规则的各个条件都为真时才触发相应的操作。
Snort解析规则时,分别生成TCP、UDP、ICMP和IP这4个不同的规则树,每一个规则树包含独立的三维链表:RTN(规则头),OTN(规则选项)和FUNC(指向匹配子函数的指针)。 当Snort捕获一个数据报时,首先对其解码,然后进行预处理,再利用规则树对数据报进行匹配。在规则树匹配过程中:根据该数据报的IP协议决定与哪个规则树进行匹配;然后与RTN结点依次进行匹配,当与某个规则头相匹配时,接着向下与OTN结点进行匹配。每个OTN结点都包含了一条规则的全部选项,它包含的一组函数指针就是用来实现对这些条件的匹配操作。当检测得知数据报与某个OTN结点的所有条件相符合时,即判断此数据报为攻击报文。
为提高规则匹配的速度,Snort采用了Boyer-Moore字符串匹配算法、二维列表递归检索(RTN和OTN)以及函数指针列表(称为“三维列表”)等方法。
三、输出插件:
抓包引擎从网络获取数据包并发送给分析模块,如果包触发了报警或日志事件,那么数据就发送给相应的输出插件。输出插件在预处理器和抓包引擎执行完之后调用Snort报警和日志子系统时执行。
Snort输出插件的功能可以分为7个部分:版权和头信息;头文件、依赖关系和全局变量;关键字注册;参数分析和函数列表链;数据处理,格式化和存储;处理预处理器参数;清理和退出。下面详细描述插件的各功能。
1、版权和头信息现存的每一个输出插件都含有鲜明的版权信息,版权信息可以由插件开发者自主添加。插件的头详细描述了插件的用途,需要的参数、结果以及其他注释。
2、头文件,依赖关系和全局变量就绝大部分应用而言,文件以及它们之间的依赖关系对程序至关重要,而且要自释其意。全局变量在整个插件的任何部分都可以使用。
3、关键字注册输出插件通过配置文件和命令行引用和调用。用户必须为插件定义关键字并把该关键字连接到Snort,以便分析该关键字时作相应的特殊处理。
4、参数分析和函数列表链大部分插件在声明时需要传递参数,因此有必要写一些代码来处理这些数据。例如,当使用日志功能时,可能需要指定一个用于存储日志的日志文件名。除了分析参数,插件还必须链接Snort主引擎内的函数。
5、数据处理、格式化和存储数据处理、格式化和存储是插件最主要的功能,可以这么说,如果没有数据处理、格式化和存储这些功能,输出插件就不完整,没有用。
6、处理预处理器参数在有预处理器参数存在时,必须写数据处理代码来处理这些参数,这样在分析开始之前,Snort和输出插件就能区分预处理器单元。
7、清理和退出,在绝大多数情况下,需要在插件中包含清理内存、应用连接以及打开套接字的退出处理代码,这样可以提高Snort的执行效率。 Snort能够使用输出插件来汇报和表示数据,Snort支持多种日志格式,包括直接的文本头、PCAP、UNIXsyslog、XML文本数据库和多种关系数据库,这些输出插件使得报警和日志以更加灵活的格式和表现形式呈现给管理员。如果没有输出插件来处理、存储和格式化数据,包分析和流量分析是没有任何意义的。
安装了source-insight4.0开始阅读源码,arp协议的复习
首先第一个看到是main函数里面的arguments里面的char指针传递的-a的指令,这个指令的解释是 -a => display ARP packets,
在以太网中,一个主机和另一个主机进行直接通信,必须要知道目标主机的MAC地址。但这个目标MAC地址是如何获得的呢?它就是通过地址解析协议获得的。所谓“地址解析”就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。ARP协议的基本功能就是通过目标设备的IP地址,查询目标设备的MAC地址,以保证通信的顺利进行。
在main函数传递了-a这个参数以后,传递到ParseCmdLine来解析,这个函数是一个死循环,
while((ch = getopt(argc, argv, "eh:l:dc:n:i:vV?aso")) != EOF)
case 'a': /* show ARP packets */
#ifdef DEBUG
printf("Show ARP active\n");
#endif
pv.showarp_flag = 1;
break;
调用了这个方法,把这个pv.showarp_flag调成了1,顺带一提,这个pv应该是控制操作指令的一个属性,里面包含着很多属性,要做什么操作都看这里控制。
snort->openpcap
/* open up our libpcap packet capture interface */
OpenPcap(pv.interface);
在上面的flag调整完以后开始使用openpcap来进行抓包,其中pv是上一步指定的操作的指令结构体,interface是抓包的接口。这里用到了一种设计模式,方便以后的更改,以及程序的可进行的操作,interface接口传进来的是char指针类型。
开始我们看到openpcap这个函数
bpf_u_int32 localnet, netmask; /* net addr holders */
struct bpf_program fcode; /* Finite state machine holder */
char errorbuf[PCAP_ERRBUF_SIZE]; /* buffer to put error strings in */
这里声明了三个变量,第一个是bpfu_int32类型的localnet,还有netmask,
参照一下csdn上面的解释,pcap简单使用和简单解释 - 青儿哥哥 - 博客园 (cnblogs.com)
数据类型bpf_u_int32实际上就是u_int的一个别名,还有吧bpf_int32实际上就是int的别名。当然这个int是32位的,如果操作系统对int的定义不是4字节,bpf_int32就对应另外一种类型,总之,bpf_u_int32就是一个32位的无符号整型。
关键函数:
int pcap_lookupnet(const char *device, bpf_u_int32 *netp,bpf_u_int32 *maskp, char *errbuf)用于获取网卡的网络号和子网掩码。其中device参数是网卡名,netp 和maskp表示将要获得的网络号和子网掩码,都是以网络字节序存放的,比如一个IP为10.108.20.0,那么netp中存放的这个地址就是:1338378。转换成二进制就是:
00000000 00010100 01101100 00001010
bpfu_int32就是用来适应不同系统的电脑的数,满足32位的int类型
接着bpf_program就是用一个限定的状态来定义机器的保持?(英语不好)
后面的errorbuff是一个错误的缓冲区。
现在开始使用里面的函数,里面给pv的接口赋值,pcap_lookupdev莺歌就是调用c语言的库,lookupdev顾名思义就是寻找硬件设备,应该是调用主机的本地的函数。
赋值了以后,就使用这个函数获得硬件的解析器
pd = pcap_open_live(pv.interface, SNAPLEN, PROMISC, READ_TIMEOUT, errorbuf);
pcap_compile(pd, &fcode, pv.pcap_cmd, 0, netmask) < 0
pcap_setfilter(pd, &fcode) < 0
判断完不为空以后就开始使用pcap_lookupnet(pv.interface, &localnet, &netmask, errorbuf) < 0来进行判断,其中的小于零是出现了错误的情况应该是硬件设备没被找到,后面的三个也是错误的判断,假如有错就丢到错误栈里面,这个有点向java的异常抛出
/* get data link type */
datalink = pcap_datalink(pd);
if (datalink < 0)
{
fprintf(stderr, "ERROR: OpenPcap() datalink grab: \n\t%s\n", pcap_geterr(pd));
exit(1);
}
这一步应该是openpcap里面最关键的一步,里面的datalink就是指定了连接的类型下一步就是跳出openpcap这个函数了。
总结一下这个函数,就是初始化openpcap的配置,把错误的东西都给弄走。
snort->setpktprocessor
/* set the packet processor (ethernet, slip or raw)*/
SetPktProcessor();
设置包的进程?(机翻)
这个函数将会返回一个int类型的数据
一开始使用了一个switch的函数
switch(datalink)
{
case DLT_EN10MB:
printf("Decoding Ethernet on interface %s\n", pv.interface);
grinder = (pcap_handler) DecodeEthPkt;
break;
case DLT_SLIP:
printf("Decoding Slip on interface %s\n", pv.interface);
if(pv.showeth_flag == 1)
{
printf("Disabling Ethernet header printout (you aren't using Ethernet!\n");
pv.showeth_flag = 0;
}
grinder = (pcap_handler) DecodeSlipPkt;
break;
这个datalink类型,是链路层的类型,借用一下csdn上面的
DLT_EN10MB: 以太网(10Mb, 100Mb, 1000Mb, 或者更高)。
DLT_SLIP:SLIP。
先来看一下第一个DLT_EN10MB里面最关键的函数就是这个DecodeEthPkt,这个函数我觉得就是解码包了,我觉得是snort里面最关键的函数了,其中的grinder翻译为n.碾磨器械; 磨刀匠; 磨工; 磨床;
这个grinder是个方法指针,接收了这个DecodeEthPkt的值,然后强转成pcap_handler类型
grinder = (pcap_handler) DecodeEthPkt;
下面开始进入decodeEthPkt这个函数,在decode文件下面
void DecodeEthPkt(char *user, struct pcap_pkthdr *pkthdr, u_char *pkt)
这个方法传入了三个参数,第一个是user,第二个是
struct pcap_pkthdr {
struct timeval ts; /* time stamp */
bpf_u_int32 caplen; /* length of portion present */
bpf_u_int32 len; /* length this packet (off wire) */
};
这个pcap_pkthdr这个结构体,有一个时间戳,还有捕捉的长度,还有包的长度。csdn上面某文章的说法–因为在某些情况下你不能保证捕获的包是完整的,例如一个包长1480,但是你捕获到1000的时候,可能因为某些原因就中止捕获了,所以caplen是记录实际捕获的包长,也就是1000,而len就是1480。
后面的是个u_char类型是传入的包
传入的数据搞定了,看一下初始化的内容
int pkt_len; /* suprisingly, the length of the packet */
int cap_len; /* caplen value */
int pkt_type; /* type of pkt (ARP, IP, etc) */
EtherHdr *eh; /* ethernet header pointer (thanks Mike!) */
/* set the lengths we need */
pkt_len = pkthdr->len; /* total packet length */
cap_len = pkthdr->caplen; /* captured packet length */
ClearDumpBuf();
bzero((void *) &pip, sizeof(PrintIP));
-
第一个是pkt_len是包的长度
-
第二个是cap_len是捕捉的长度
-
第三个是包的类型,分别有arp包,ip包,etc包etc包就是以太帧
1:版本号 4 bit 2:头长度 4 bit 3:服务类型 8 bit 4:总长度 16 bit 5:标识 16 bit 6:标志 4 bit 7:片移量 12 bit 8:生存时间 8 bit 9:上层协议标识 8 bit 10:头部校验和 16 bit 11:源地址 bit 12:目标地址 32 bit 共计:20字节
有点忘记了Ethernet帧是什么样了,现在看一哈
以太帧中还包括源和目的MAC地址,分别代表发送者的MAC和接收者的MAC,此外还有帧校验序列字段,用于检验传输过程中帧的完整性。
以太网在二层链路上通过MAC地址来唯一标识网络设备,并且实现局域网上网络设备之间的通信。MAC地址也叫物理地址,大多数网卡厂商把MAC地址烧入了网卡的ROM中。发送端使用接收端的MAC地址作为目的地址。以太帧封装完成后会通过物理层转换成比特流在物理介质上传输。
网络设备如何确定以太网数据帧的上层协议?
以太网帧中包含一个Type字段,表示帧中的数据应该发送到上层哪个协议处理。比如,IP协议对应的Type值为0x0800,ARP协议对应的Type值为0x0806。
终端设备接收到数据帧时,会如何处理?
主机检查帧头中的目的MAC地址,如果目的MAC地址不是本机MAC地址,也不是本机侦听的组播或广播MAC地址,则主机会丢弃收到的帧。如果目的MAC地址是本机MAC地址,则接收该帧,检查帧校验序列(FCS)字段,并与本机计算的值对比来确定帧在传输过程中是否保持了完整性。如果检查通过,就会剥离帧头和帧尾,然后根据帧头中的Type字段来决定把数据发送到哪个上层协议进行后续处理。
里面调用了
ClearDumpBuf();//就是清除缓冲区
void ClearDumpBuf()
{
if(data_dump_buffer != NULL)
free(data_dump_buffer);
data_dump_buffer = NULL;
dump_ready = 0;
}
bzero((void *) &pip, sizeof(PrintIP));
bzero(&net, sizeof(NetData));//两个都填0的初始化
其中的pip是PrintIP类型,然后net是NetData类型的
/* do a little validation */
if(cap_len < ETHERNET_HEADER_LEN)//这里是做合法性检测的
//看看以太网帧是不是比捕捉的帧大,大的话就溜了溜了
/* lay the ethernet structure over the packet data */
pkt_type = ntohs(eh->ether_type);
if(pv.showeth_flag)
{
memcpy(pip.eth_src, eh->ether_src, 6);
memcpy(pip.eth_dst, eh->ether_dst, 6);
pip.eth_len = pkt_len;
pip.eth_type = pkt_type;
}
上面的这一段代码是假如是showeth为真的话,就把6位的eh->ether_src复制到pip里面
/* set the packet index pointer */
设置包的指针
pktidx = pkt;
增加一个ethernet的头部的长度,因为我们现在注重的是包的内容
/* increment the index pointer to the start of the network layer */
pktidx += ETHERNET_HEADER_LEN;
decode里面应该是最重要的一环
switch(pkt_type)
{
case ETHERNET_TYPE_IP:
#ifdef DEBUG
printf("IP Packet\n");
#endif
DecodeIP(pktidx, pkt_len-ETHERNET_HEADER_LEN);
return;
case ETHERNET_TYPE_ARP:
case ETHERNET_TYPE_REVARP:
if(pv.showarp_flag)
DecodeARP(pktidx, pkt_len-ETHERNET_HEADER_LEN, cap_len);
return;
case ETHERNET_TYPE_IPX:
DecodeIPX(pktidx, (pkt_len-ETHERNET_HEADER_LEN));
return;
default:
return;
}
这里是筛选pkt的类型
1.ETHERNET_TYPE_IP–DecodeIP(…)
初始的东西
这里的代码是真的长- -
先看传入的参数把
**u_char *pkt, **—>ptr to the packet data
const int len—>length from here to the end of the packet
慢慢来
IPHdr *iph; /* ip header ptr ip的头指针*/
u_int ip_len; /* length from the start of the ip hdr to the pkt end ip包的长度 */
u_int hlen; /* ip header length ip头的长度*/
/* lay the IP struct over the raw data */
这里是将字符类型的pkt强制转换为IPHdr结构,raw是"生,未处理"的数据
iph = (IPHdr *) pkt;
合法性的操作
/* do a little validation */
if(len < sizeof(IPHdr))
{
if(pv.verbose_flag)
fprintf(stderr, "Truncated header! (%d bytes)\n", len);
return;
}
首先这里的合法性是,这里的包是被删减过的意思(Truncated)头部的包的合法性检测
if(len < ip_len)
{
/*verbose是冗长的意思*/
if(pv.verbose_flag)
{
fprintf(stderr,
"Truncated packet! Header says %d bytes, actually %d bytes\n",
ip_len, len);
PrintNetData(stdout, pkt, len);
}
if(pv.log_flag)
{
OpenLogFile(DUMP);
PrintNetData(log_ptr, pkt, len);
fflush(log_ptr);
fclose(log_ptr);
}
return;
}
这里的合法性检测是,包是被删减的,根据包头的说明,包里面少了点东西。
正式的开始解包!
/* set the IP header length */
hlen = iph->ip_hlen * 4;
这里是准备的操作,把ip_hlen*4赋值到hlen
if(hlen > 20)
{
DecodeIPOptions( (pkt + 20), hlen - 20);
}
DecodeIPOptions( (pkt + 20), hlen - 20);
这里有两个参数
- Arguments: o_list => ptr to the option list
-
o_len => length of the option list
ip首部长度20所以要减去20,相当于指针位移了
这里穿进去的o_len是hlen-20,cp是包
先来复习一下ip的options
TCP头部和IPV4头部除了固定的20字节外,都设置了 OPTION 字段用于存储自定义的数据,因为TCP头部和IPV4的报文长度字段均为4字节,所表示的最大值为15, 乘4,报文头部最大长度为60字节,因此Option字段最大长度为40字节,足够存储大量的报文控制信息。TCP和IPV4 OPTION的格式均为(标识字段 - 长度 - 数据)格式,一般采取4字节对齐存储。
重要的解包的过程
第一步,读取出option字段
**可选项(Options):**这是一个可变长的字段。该字段属于可选项,主要用于测试,由起源设备根据需要改写。可选项目包含以下内容:
松散源路由(Loose source routing):给出一连串路由器接口的IP地址。IP包必须沿着这些IP地址传送,但是允许在相继的两个IP地址之间跳过多个路由器。
严格源路由(Strict source routing):给出一连串路由器接口的IP地址。IP包必须沿着这些IP地址传送,如果下一跳不在IP地址表中则表示发生错误。
路由记录(Record route):当IP包离开每个路由器的时候记录路由器的出站接口的IP地址。
时间戳(Timestamps):当IP包离开每个路由器的时候记录时间。
while (o_len > 0)
{
/* Check for zero length options */
//这里不怎懂,因为对ip包陌生。
opt = *cp++;
if((opt == IPOPT_EOL) ||
(opt == IPOPT_NOP))
{
len = 1;
}
else
{
len = *cp++; /* total including type, len */
if(len < 2 || len > o_len)
{
if(pv.verbose_flag)
{
printf("Illegal IP options: option says %d, IP header says %d\n", len, o_len);
PrintNetData(stdout, o_list, o_len);
}
if(pv.log_flag)
{
OpenLogFile(BOGUS);
fprintf(log_ptr, "Illegal IP options: option says %d, IP header says %d\n", len, o_len);
PrintNetData(log_ptr, o_list, o_len);
fclose(log_ptr);
}
break;
}
/* account for length byte */
o_len--;
}
/* account for type byte */
o_len--;
/* Handle the rest of the options */
datalen = 0;
switch (opt)
{
case IPOPT_RR:
datalen = len - 2;
strncat(pip.IPO_Str, "RR ", 3);
break;
case IPOPT_EOL:
strncat(pip.IPO_Str, "EOL ", 4);
break;
case IPOPT_NOP:
strncat(pip.IPO_Str, "NOP ", 4);
break;
case IPOPT_TS:
datalen = len - 2;
strncat(pip.TO_Str, "TS ", 3);
break;
case IPOPT_SECURITY:
datalen = len - 2;
strncat(pip.TO_Str, "SEC ", 4);
break;
case IPOPT_LSRR:
case IPOPT_LSRR_E:
datalen = len - 2;
strncat(pip.TO_Str, "LSRR ", 5);
break;
case IPOPT_SATID:
datalen = len - 2;
strncat(pip.TO_Str, "SID ", 4);
break;
case IPOPT_SSRR:
datalen = len - 2;
strncat(pip.TO_Str, "SSRR ", 5);
break;
default:
datalen = len - 2;
sprintf(tmpbuf, "Opt %d:", opt);
strncat(pip.TO_Str, tmpbuf, strlen(tmpbuf));
for(i = 0; i < datalen; ++i)
{
sprintf(tmpbuf, " %02x ", cp[i]);
strncat(pip.TO_Str, tmpbuf, strlen(tmpbuf));
}
break;
}
/*
* Account for data printed
*/
cp += datalen;
o_len -= datalen;
}
这里根据读取的东西,知道了ip的option字段是什么。
开始给结果赋值
/* generate a timestamp */
/*GetTime(pip.timestamp);*/
/* start filling in the printout data structures */
strncpy(pip.saddr, inet_ntoa(iph->ip_src), 15);//这里复制了15位为什么是15位呢,因为
//最后一位被保留,恒定为0 IP包总长(Total Length):长度16比特
strncpy(pip.daddr, inet_ntoa(iph->ip_dst), 15);
net.sip = iph->ip_src.s_addr;//源地址
net.dip = iph->ip_dst.s_addr;//目的地
pip.ttl = iph->ip_ttl;//生存时间
pip.tos = iph->ip_tos;//见下面|||||
|||||
vvvv
/* check for fragmented packets */
ip_len -= hlen;
pip.frag_off = ntohs(iph->ip_off);
TOS
ToS即为服务类型,只有当网络设备能够支持(能够识别IP首部中的ToS字段)识别ToS字段时,这给字段设置才有意义。否则都是空谈。
先说具体字段的意义:
Tos字段长度为8bit
pip.ip_df = (pip.frag_off & 0x4000) >> 14;df是是否设置分片//这里与了0x4000这里右移了14位,
pip.ip_mf = (pip.frag_off & 0x2000) >> 13;
/** more fragment,在IP数据报中FLAGS中的分片标志位,位于IP数据报中的第50比特位,分为MF和DF(DF为第49位,第48位暂未使用,无实际意义),MF表示more fragment即还有分片,DF表示don't fragment即没有分片。
more fragment,在IP数据报中FLAGS中的分片标志位,分为MF和DF,MF表示more fragment即还有分片,DF表示dont't fragment即没有分片。*/
pip.frag_off &= 0x1FFF;12+1位的数据
pip.frag_id = ntohs(iph->ip_id);
if(pip.frag_off == 0)
//假如没有分片
{
#ifdef DEBUG
printf("IP header length: %d\n", hlen);
#endif
/* move the packet index to point to the transport layer */
pktidx = pktidx + hlen;
switch(iph->ip_proto)
{
//区分协议
case IPPROTO_TCP:
net.proto = IPPROTO_TCP;
strncpy(pip.proto, "TCP", 3);
DecodeTCP(pktidx, len-hlen);
return;
case IPPROTO_UDP:
net.proto = IPPROTO_UDP;
strncpy(pip.proto, "UDP", 3);
DecodeUDP(pktidx, len-hlen);
return;
case IPPROTO_ICMP:
net.proto = IPPROTO_ICMP;
strncpy(pip.proto, "ICMP", 4);
DecodeICMP(pktidx, len-hlen);
return;
default:
return;
}
}
else
{
switch(iph->ip_proto)
{
case IPPROTO_TCP:
net.proto = IPPROTO_TCP;
strncpy(pip.proto, "TCP", 3);
break;
case IPPROTO_UDP:
net.proto = IPPROTO_UDP;
strncpy(pip.proto, "UDP", 3);
break;
case IPPROTO_ICMP:
net.proto = IPPROTO_ICMP;
strncpy(pip.proto, "ICMP", 4);
break;
default:
net.proto = IPPROTO_ICMP;
strncpy(pip.proto, "UNK", 3);
break;
}
/* move the packet index to point to the transport layer */
pktidx = pktidx + hlen;
pip.data = pktidx;
pip.dsize = len - hlen;
if(pv.verbose_flag)
{
PrintIPPkt(stdout, net.proto);
}
if(pv.log_flag)
{
OpenLogFile(0);
PrintIPPkt(log_ptr, net.proto);
fclose(log_ptr);
}
}
ip解析的差不多了,开始走udp,tcp,icmp和ump的解析了,上面解析了ip的头
小结,学习了ip的协议,对ip头又有了了解了。
下面开始1.5分析
ps:由于source insign已经过期了所以我选择用新的源码插卡器来看,这里用的是sourcetrail
首先肯定是从snort.c文件开始分析
snort.c的结构是这样的,我们就从main函数开始看
第一步是初始化各种参数,来方便后面程序的调用,例如netmask protonames等
signal(SIGKILL, CleanExit);
signal(SIGTERM, CleanExit);
signal(SIGINT, CleanExit);
signal(SIGQUIT, CleanExit);
signal(SIGHUP, CleanExit);
/* set a global ptr to the program name so other functions can tell
what the program name is */
progname = argv[0];
InitNetmasks();
InitProtoNames();
/*
InitPreprocessors();
DumpPreprocessors();
InitPlugIns();
DumpPlugIns();
*/
/* initialize the packet counter to loop forever */
pv.pkt_cnt = -1;
/* set the default alert mode */
pv.alert_mode = ALERT_FULL;
/* set the timezone (ripped from tcpdump) */
thiszone = gmt2local(0);
第二步就是处理指令的ParseCmdLine(argc, argv);
这个函数我们在1.0的时候已经见过了,就是把传进来的参数进行处理,然后变成格式化的参数来供后面来使用,由于这个函数是在snort.c里面创建的,所以可以接触到里面的全局变量,这样的话就为参数的传递提供便利
同时这个函数里面自带一个循环,所以在输入终止指令的时候会一直的走
在处理完指令以后就是一大坨的合法性检验啊什么的
if(pv.use_rules || pv.log_flag)
{
/* perform some sanity checks on the output directory, etc*/
SanityChecks();
}
if(!pv.logbin_flag)
{
if(!pv.nolog_flag)
LogFunc = LogPkt;
else
LogFunc = NoLog;
}
if(pv.use_rules && pv.rules_order_flag)
{
printf("Rule application order changed to Pass->Alert->Log\n");
}
if(!pv.use_rules && !pv.verbose_flag && !pv.log_flag)
{
printf("\n\nUh, you need to tell me to do something....\n\n");
ShowUsage(progname);
exit(0);
}
if(pv.syslog_flag)
{
AlertFunc = SyslogAlert;
}
else if(pv.smbmsg_flag)
{
#ifdef ENABLE_SMB_ALERTS
AlertFunc = SmbAlert;
#else
fprintf(stderr, "ERROR: SMB support not compiled into program, exiting...\n");
exit(1);
#endif
}
else
/* Tell 'em who wrote it, and what "it" is */
DisplayBanner();
这里还有一串很傻的代码,估计是github上面哪个想浑水摸鱼的写的hhh,特地写了个只有一行的函数
第三步就是开始处理刚刚传入的指令了。int OpenPcap(char *intf)
if(!pv.readmode_flag)
{
/* open up our libpcap packet capture interface */
OpenPcap(pv.interface);
}
else
{
OpenPcap(pv.readfile);
}
这里的pv是已经被处理过的了,里面有我们想干的模式,这个openpcap和1.0版本的应该是大同小异,就是开始抓包,参数就是选择调用哪个接口
libpcap
libpcap使用流程借用csdn
决定对那一个接口进行嗅探,如eth0。我们也可以用一个字符串来定义这个设备。
初始化pcap。使用文件句柄传入需要嗅探的设备。同时支持多个设备的嗅探。
设置BPF. 创建一个规则集合,编译并且使用它。这个过程分为三个阶段:
1.规则集合被置于一个字符串内,并且被转换成能被pcap读的格式。
2.编译该规则(就是调用一个不被外部程序使用的函数)。
3.告诉pcap使用它来过滤数据包。
pcap进入它的主循环。在这个阶段内pcap一直工作到它接收了所有我们想要的包为止。每当它收到一个包(或者多个数据包)就调用另一个已经定义好的函数,这个函数可以做我们想要的任何工作,比如它可以剖析包的上层协议信息并给用户打印出结果,它可以将结果保存为一个文件,或者什么也不作。
在嗅探到所需的数据后,我们要关闭会话并结束。
第四步就是 SetPktProcessor();
这一步是设置信息包处理器 一共有五种的形态
DLT_EN10MB
grinder = (pcap_handler) DecodeEthPkt;
DLT_IEEE802
grinder = (pcap_handler) DecodeTRPkt;
DLT_SLIP
grinder = (pcap_handler) DecodeSlipPkt;
DLT_PPP
grinder = (pcap_handler) DecodePppPkt;
DLT_NULL
grinder = (pcap_handler) DecodeNullPkt;
这里一共有五种形态,grinder的意思是碾磨器械; 磨刀匠; 磨工; 磨床;,在这里就是处理包所使用的方法,然后后面的强制类型转换,再加上想要的类型,这个有点像面向对象编程的方法,(pcap_handler)是一个接口,然后在调用这个的时候,实际调用的是实现这个类的实际方法
这个类就不细说了,里面有的方法已经在1.0就存在了。
第五步就是pcap_loop
if(pcap_loop(pd, pv.pkt_cnt, grinder, NULL) < 0)
{
if(pv.daemon_flag)
syslog(LOG_CONS|LOG_DAEMON,"pcap_loop: %s", pcap_geterr(pd));
else
fprintf(stderr, "pcap_loop: %s", pcap_geterr(pd))
CleanExit();
}
这里是一个c里面原生的一个类里面的方法
函数名称:int pcap_loop(pcap_t * p,int cnt, pcap_handler callback, uchar * user);
函数功能:捕获数据包,不会响应pcap_open_live()函数设置的超时时间
参数说明:p 是由pcap_open_live()返回的所打的网卡的指针;cnt用于设置所捕获数据包的个数;pcap_handler 是与void packet_handler()使用的一个参数,即回调函数的名称;user值一般为NULL
pcap_loop原型是pcap_loop(pcap_t *p,int cnt,pcap_handler callback,u_char *user)
其中第一个参数是winpcap的句柄,第二个是指定捕获的数据包个数,如果为-1则无限循环捕获。第四个参数user是留给用户使用的。第三个是回调函数其原型如下:
pcap_callback(u_char* argument,const struct pcap_pkthdr* packet_header,const u_char* packet_content)
其中参数packet_content表示的捕获到的数据包的内容参数argument是从函数pcap_loop()传递过来的。
注意:这里的参数就是指 pcap_loop中的 *user 参数参数
pcap_pkthdr 表示捕获到的数据包基本信息,包括时间,长度等信息.
另外:回调函数必须是全局函数或静态函数,其参数默认,
其中四个参数的赋值情况是
pd->winpcap的句柄
/* 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);
cnt->包的个数
/* 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);
总结:在宏观上,main函数大体上和1.0没什么区别
下面是有关于snort的规则的配置
我们首先发现snort.h里面有个全局变量叫use_rules,他被这6个东西调用,并且这个是个int类型的变量,也就是说这个东西肯定与是否使用规则有关,那么按图索骥,可以从上面的6个调用的地方来知道与规则有关的详细的配置。
分析DecodeIcmp(decode.c)
首先这个DecodeIcmp被调用的时机应该是之前的grinder里面的方法,会自动的在loop里面被调用
首先是关键代码
这里直接看最外层的ifelse语句,pv.use_rules是真的,为1的话就会执行Preprocess这个语句
而这个就是预处理。
if(!pv.use_rules)
{
if(pv.log_flag)
{
if(pv.logbin_flag)
LogBin(p);
else
LogPkt(p);
}
}else
{
Preprocess(p);
}
分析Preprocess函数
void Preprocess(Packet *p)
{
PreprocessFuncNode *idx;
do_detect = 1;
idx = PreprocessList;
while(idx != NULL)
{
idx->func(p);
idx = idx->next;
}
if(!p->frag_flag && do_detect)
{
Detect(p);
}
}
这里首先一眼扫过去有个->的符号,而且还出现了next,Node这个字样,那么显而易见就是一个链表的数据了,有了这个思路我们再对PreprocessFuncNode这个类型的分析就会有头绪了,
首先这是一个链表,为什么会使用链表呢,我认为是因为处理规则是一种流水线一样的操作,是属于线性结构的,为了方便增删,减少内存的消耗,用链式来储存是比较好的方法,我们下面来看这个函数,,
首先是idx这个指针,是用来方便使用的,用来记录当前指向的地方是哪里,他指向的preprocesslist这个是一个全局变量,在别的地方被赋值,后面经跟着的是一个链表的遍历,每次都会调用一下func这个方法,对packet p 来进行操作,我们来看看func怎么玩
对preprocessList进行分析
首先这玩意是个funcNode的结构体类型
typedef struct _PreprocessFuncNode
{
void (*func)(Packet *);
struct _PreprocessFuncNode *next;
} PreprocessFuncNode;
里面包含着返回值为void的一个叫func接收Packet参数的一个方法,还有下一个节点。
这个类型就是用来存储方法节点的。
我们来看看这个东西在哪里被调用
一个是addFuncToPreprocList的方法,一个是Preprocess方法,第一个是增加,第二个是实施。
我们先来看看增加
这里就是一个普普通通的链表增加的方法了,假如是空的话就创个头,calloc第一次看见,之前都是用malloc的,malloc和calloc的区别是:
主要的不同是malloc不初始化分配的内存,calloc初始化已分配的内存为0。
次要的不同是calloc返回的是一个数组,而malloc返回的是一个对象。
calloc等于malloc后在memset很可能calloc内部就是一个malloc再来一个memset清0。
所以malloc比calloc更高效.
void AddFuncToPreprocList(void (*func)(Packet *))
{
PreprocessFuncNode *idx;
idx = PreprocessList;
if(idx == NULL)
{
PreprocessList = (PreprocessFuncNode *) calloc(sizeof(PreprocessFuncNode), sizeof(char));
PreprocessList->func = func;
}
else
{
while(idx->next != NULL)
idx = idx->next;
idx->next = (PreprocessFuncNode *) calloc(sizeof(PreprocessFuncNode), sizeof(char));
idx = idx->next;
idx->func = func;
}
return;
}
这个方法被以上三个地方调用
HttpDecodeInit(u_char *args)的分析 来自于spp_http_decode.c
这个函数传入一个字符串
总结
Snort规则是基于文本的,它通常存在于Snort程序目录中或者子目录中。在启动的时候,Snort读取所有的规则文件,并且建立一个三维的链表。Snort使用列表匹配包和检测。
1、规则解析:
Snort规则是Snort入侵检测系统的重要组成部分。规则集是snort的攻击特库,每条规则都对应一条攻击特征,snort通过它来识别攻击行为。
每一条规包括两个部分:规则头部(RuleHeader)和规则选项(RuleOption)。规则头包含规则的行为、协议、源地址、目的地址、子网掩码、源和目的端口信息。规则选项包含报警信息以及规则触发时提供给管理员的参考信息。例如: alerttcpanyany->202.203.112.0/24any(content:“proxy-connection”;msg:“proxyuse”😉 这条规则述信息是:对任何访问202.203.112网段,tcp报文中含有proxy-connection的流量报警。在这个例子中,左括号前面是规则头。圆括号内的内容是规则选项,在规则选项中,content后面的内容为关键字,及需要匹配的字符串,msg后面的内容为规则触发时将显示的信息。Snort的规则定义中可以没有规则体,它们只是用来更好地定义所要进行的某种处理(记录、报警、忽略等)的数据包类型。只有当规则中的每一个元素都为真时,才能触发对应的规则动作。
2、检测引擎:
Snort把具有相同条件的规则链接到一个集合中,用RTN结构来描述;规则选项对应于规则选项结点OTN(OptionalTreeNode),包含一些特定的检测标志、报警信息、匹配内容等条件,每个选项的匹配子函数(插件)放到FUNC链表中。只有当规则的各个条件都为真时才触发相应的操作。
Snort解析规则时,分别生成TCP、UDP、ICMP和IP这4个不同的规则树,每一个规则树包含独立的三维链表:RTN(规则头),OTN(规则选项)和FUNC(指向匹配子函数的指针)。 当Snort捕获一个数据报时,首先对其解码,然后进行预处理,再利用规则树对数据报进行匹配。在规则树匹配过程中:根据该数据报的IP协议决定与哪个规则树进行匹配;然后与RTN结点依次进行匹配,当与某个规则头相匹配时,接着向下与OTN结点进行匹配。每个OTN结点都包含了一条规则的全部选项,它包含的一组函数指针就是用来实现对这些条件的匹配操作。当检测得知数据报与某个OTN结点的所有条件相符合时,即判断此数据报为攻击报文。
为提高规则匹配的速度,Snort采用了Boyer-Moore字符串匹配算法、二维列表递归检索(RTN和OTN)以及函数指针列表(称为“三维列表”)等方法。
snort的输出插件
输出插件:
抓包引擎从网络获取数据包并发送给分析模块,如果包触发了报警或日志事件,那么数据就发送给相应的输出插件。输出插件在预处理器和抓包引擎执行完之后调用Snort报警和日志子系统时执行。
Snort输出插件的功能可以分为7个部分:版权和头信息;头文件、依赖关系和全局变量;关键字注册;参数分析和函数列表链;数据处理,格式化和存储;处理预处理器参数;清理和退出。下面详细描述插件的各功能。
1、版权和头信息现存的每一个输出插件都含有鲜明的版权信息,版权信息可以由插件开发者自主添加。插件的头详细描述了插件的用途,需要的参数、结果以及其他注释。
2、头文件,依赖关系和全局变量就绝大部分应用而言,文件以及它们之间的依赖关系对程序至关重要,而且要自释其意。全局变量在整个插件的任何部分都可以使用。
3、关键字注册输出插件通过配置文件和命令行引用和调用。用户必须为插件定义关键字并把该关键字连接到Snort,以便分析该关键字时作相应的特殊处理。
4、参数分析和函数列表链大部分插件在声明时需要传递参数,因此有必要写一些代码来处理这些数据。例如,当使用日志功能时,可能需要指定一个用于存储日志的日志文件名。除了分析参数,插件还必须链接Snort主引擎内的函数。
5、数据处理、格式化和存储数据处理、格式化和存储是插件最主要的功能,可以这么说,如果没有数据处理、格式化和存储这些功能,输出插件就不完整,没有用。
6、处理预处理器参数在有预处理器参数存在时,必须写数据处理代码来处理这些参数,这样在分析开始之前,Snort和输出插件就能区分预处理器单元。
7、清理和退出,在绝大多数情况下,需要在插件中包含清理内存、应用连接以及打开套接字的退出处理代码,这样可以提高Snort的执行效率。 Snort能够使用输出插件来汇报和表示数据,Snort支持多种日志格式,包括直接的文本头、PCAP、UNIXsyslog、XML文本数据库和多种关系数据库,这些输出插件使得报警和日志以更加灵活的格式和表现形式呈现给管理员。如果没有输出插件来处理、存储和格式化数据,包分析和流量分析是没有任何意义的。
输出插件使得Snort在向用户提供格式化输出时更加灵活。输出插件在Snort的报警和记录子系统被调用时进行,在预处理和探测引擎之后。规则文件中指令的格式非常类似于预处理程序。
格式:
output:
例如:
output alert_syslog:LOG_AUTHLOG_ALERT
总结
1.5版本相较于1.0版本多了两个插件,第一个是预处理器插件,第二个是处理插件。
两个插件的作用分别是:
预处理器插件模块
在数据包进入检测引擎之前, 它们首先被送入预处理器 , 主要是在数据包送入 Snort主检测引擎前提供一个报警、丢弃数据包、修改数据包的地方。预处理器完成的功能主要有:①包重组, 完成将多个包中的数据进行合并的工作, 如 TCP流重组插件 (stream4)、 IP 碎片重组插件 (frag2);②协议解码和规范化, Snort中有三个协议解码及规范化插件, 分别对 Telne t、HTTP和 RPC 协议进行处理;③异常检测, 对无法用一般的规则发现的攻击进行检测, 或者进行协议异常检测, 如端口扫描插件、Back Orifice检测插件等;在 Snort.conf文件中加入preprocessor 就可以启动所需的预处理器。
处理插件模块
主添加。插件的头详细描述了插件的用途,需要的参数、结果以及其他注释。
2、头文件,依赖关系和全局变量就绝大部分应用而言,文件以及它们之间的依赖关系对程序至关重要,而且要自释其意。全局变量在整个插件的任何部分都可以使用。
3、关键字注册输出插件通过配置文件和命令行引用和调用。用户必须为插件定义关键字并把该关键字连接到Snort,以便分析该关键字时作相应的特殊处理。
4、参数分析和函数列表链大部分插件在声明时需要传递参数,因此有必要写一些代码来处理这些数据。例如,当使用日志功能时,可能需要指定一个用于存储日志的日志文件名。除了分析参数,插件还必须链接Snort主引擎内的函数。
5、数据处理、格式化和存储数据处理、格式化和存储是插件最主要的功能,可以这么说,如果没有数据处理、格式化和存储这些功能,输出插件就不完整,没有用。
6、处理预处理器参数在有预处理器参数存在时,必须写数据处理代码来处理这些参数,这样在分析开始之前,Snort和输出插件就能区分预处理器单元。
7、清理和退出,在绝大多数情况下,需要在插件中包含清理内存、应用连接以及打开套接字的退出处理代码,这样可以提高Snort的执行效率。 Snort能够使用输出插件来汇报和表示数据,Snort支持多种日志格式,包括直接的文本头、PCAP、UNIXsyslog、XML文本数据库和多种关系数据库,这些输出插件使得报警和日志以更加灵活的格式和表现形式呈现给管理员。如果没有输出插件来处理、存储和格式化数据,包分析和流量分析是没有任何意义的。
输出插件使得Snort在向用户提供格式化输出时更加灵活。输出插件在Snort的报警和记录子系统被调用时进行,在预处理和探测引擎之后。规则文件中指令的格式非常类似于预处理程序。
格式:
output:
例如:
output alert_syslog:LOG_AUTHLOG_ALERT
总结
1.5版本相较于1.0版本多了两个插件,第一个是预处理器插件,第二个是处理插件。
两个插件的作用分别是:
预处理器插件模块
在数据包进入检测引擎之前, 它们首先被送入预处理器 , 主要是在数据包送入 Snort主检测引擎前提供一个报警、丢弃数据包、修改数据包的地方。预处理器完成的功能主要有:①包重组, 完成将多个包中的数据进行合并的工作, 如 TCP流重组插件 (stream4)、 IP 碎片重组插件 (frag2);②协议解码和规范化, Snort中有三个协议解码及规范化插件, 分别对 Telne t、HTTP和 RPC 协议进行处理;③异常检测, 对无法用一般的规则发现的攻击进行检测, 或者进行协议异常检测, 如端口扫描插件、Back Orifice检测插件等;在 Snort.conf文件中加入preprocessor 就可以启动所需的预处理器。
处理插件模块
处理插件在规则匹配阶段的 ParseRleOptions函数(在 rule. c中)中被调用, 辅助完成基于规则的匹配过程。 每个规则处理函数通常对应规则选项中的一个关键字, 实现对这个关键字的解释, 主要功能为:①检查协议各字段,如TCPF lag、 ICMPType、 IPId 等;②辅助功能, 如会话记录、攻击响应、关闭连接等。