|
使用 Snort 和 PHP 构建一个小型网络防御系统 | | 级别: 初级
王丽辉 ([email=gailya@sohu.com?subject=使用 Snort 和 PHP 构建一个小型网络防御系统]gailya@sohu.com[/email]), 硕士研究生, 四川大学计算机学院
2005 年 7 月 01 日
本文在 Linux 环境下,利用 Snort 和 Iptables 构建了一个小型网络防御系统,由PHP 页面提供了一个远程管理工具,并给出关键程序的实现和说明。 引言
Snort 是目前十分流行的轻型入侵检测系统。但是目前人们对Snort检测结果的处理大都停留在记录日志或简单通知网络管理员,由管理员进行审计再决定网络防御策略的阶段。Snort的检测结果并没有及时地用来抵御网络入侵。本文通过为Snort的报警输出模块提供一个服务监听程序的办法,及时获取Snort的报警信息并对其进行解析,根据解析结果向iptables添加相应的防火墙规则,达到实时阻止网络攻击的目的。服务程序同时监听来自PHP的管理请求,并进行相应的操作。由于服务程序独立于Snort而运行,故不会影响到Snort的运行效率。管理员可以通过PHP页面对服务程序阻塞的主机进行监控和管理,如查看当前阻塞的主机IP地址、事件发生时间、阻塞时间、阻塞原因,修改阻塞时间等。
总体思想
Snort的输出plugin为我们提供了丰富的报警输出方式:输出到文件,syslog,数据库,Unix域Socket等。其中当Snort的报警输出到Unix域Socket时,输出模块相当于一个报警的客户端,Snort的用户可以通过编写服务器端代码,获取Snort的报警输出消息,并根据这些消息采取相应的对策。本文基于这种思想,并利用Linux下自带的防火墙Iptables,构建了一个网络防御系统,系统总体结构如下:
服务处理程序是该系统的核心部分,包括报警输出处理子程序(AO_Handler)和PHP请求处理子程序(Web_Handler)两个部分。报警输出处理子程序主要负责接收Snort报警,解析报警信息,并通过向iptables加规则的方式对攻击主机进行阻塞;而PHP请求处理子程序主要负责与PHP管理页面通信,并根据页面的请求做出相应的处理。
服务程序中维护了一个规则相关信息表,这个表以队列(报警队列和阻塞队列)的数据结构进行组织,其中报警队列中记录的是经Snort报警输出解析得到的攻击主机信息(即报警结点,由AO_Handler生成), 而阻塞队列中记录了正在被阻塞的攻击主机信息(即阻塞结点)。如果报警结点不在阻塞队列中,程序将对这个结点所代表的主机进行阻塞操作,并把该结点加入到阻塞队列中。对每个阻塞操作所对应的阻塞结点设定一个有效期,当阻塞时间超过这个有效期时,程序将该阻塞结点从阻塞队列中删除,并删除iptables中对应的阻塞规则,对该阻塞主机放行。采用两个列队来维护报警和阻塞信息可以防止程序在多次收到有关同一个主机地址的报警时,添加重复的阻塞规则而为规则管理和取消阻塞操作时所带来的混乱。另外阻塞信息的维护使得程序可以为终端用户提供当前被系统自动阻塞的主机的所有信息:报警时间,阻塞时间,阻塞原因等,为用户了解和管理这些信息提供了接口。
具体实现
服务程序实现方式
服务程序需要同时处理三种事件:监听Snort报警输出、维护规则相关信息表(即报警队列和阻塞队列)、处理来自PHP页面的请求。因此,服务程序采用多线程并发模式。由主线程监听来自Snort的报警数据,解析该数据、创建报警结点并将其加入报警队列,另外主线程还执行服务程序的初始化工作;由两个线程来专门维护规则相关信息表,一个线程(AlertHandler)用于检测报警队列,当其中有符合要求的报警结点时,将其加入阻塞队列,执行阻塞操作,另一个(BlockHanlder)用于监测阻塞队列,将其中到达阻塞有效期的阻塞节点删除;对于来自PHP页面的请求,服务程序采用建立一个线程池的办法,对PHP请求做出相应操作,如:列表阻塞主机,删除某阻塞主机,修改阻塞有效时间等。
主线程和规则维护线程构成了报警输出处理子程序, 而PHP请求处理线程构成了PHP请求处理子程序。由于这些线程都涉及到对规则相关信息表的读写操作,因此,需要一定的同步机制来保证操作的正确性。本文中的程序采用了建立互斥锁和条件变量的办法来达到这个目的。程序主要数据结构默认情况下,Snort的报警套接字以数据报方式向path为/var/log/snort_alert的Unix域套接字发送报警数据,报警数据被封装在一个Alertpkt的结构体中(在Snort源码包的Spo_alert_unixsock.h中定义),Alertpkt的定义如下:
typedef struct _Alertpkt{ u_int8_t alertmsg[ALERTMSG_LENGTH]; /* 报警消息 */ struct pcap_pkthdr pkth; u_int32_t dlthdr; /* datalink header offset. (ethernet, etc.. ) */ u_int32_t nethdr; /* network header offset. (ip etc...) */ u_int32_t transhdr; /* transport header offset (tcp/udp/icmp ..) */ u_int32_t data; u_int32_t val; /*指出有效的字段*/ …… Event event; /* 报警事件的相关信息 */} Alertpkt; |
报警/阻塞结点HNInfo、报警队列和阻塞队列的数据结构定义如下:
typedef struct _HNInfo{ int event_id; // 结点ID char blockIP[16]; //阻塞主机的IP地址 time_t timestamp; // 阻塞开始时间 int blocktime; // 主机阻塞时间 char msg[500]; // 阻塞原因 char desIP[16]; // 引起报警的包的目的地址 char protocol[5]; // tcp, udp or icmp uint16_t srcPort; // 源端口 uint16_t desPort; // 目的端口 struct _HNInfo* next; // 指向下一个结点} HNInfo; // 队列里的结点结构typedef struct _HNQueue{ struct _HNInfo *head, *tail; int count;} AlertQ; // 报警队列typedef struct AlertQ BlockQ; // 阻塞队列 |
报警输出处理子程序
报警输出子程序(AO_Handler)由主线程和规则信息表维护线程组成。AO_Handler在/var/log/snort_alert上建立监听套接字与Snort的报警输出模块通信,它的处理流程如下:
1) 接收Snort的报警输出,根据Alertpkt的内容,解析出引发报警的源地址,报警内容,报警事件ID号等必要信息,并将其连同报警发生的时间、需要阻塞的时间等信息并形成报警结点放入报警队列;
2) 检测报警队列,当其不为空时,取出报警结点并与阻塞队列中的结点进行比较,如果报警的主机地址已出现在阻塞队列中,说明该主机已经被阻塞,只需修改阻塞队列中该主机的报警时间戳,使其与现在的报警结点的时间戳相等;如果报警主机未出现,则将报警结点加入阻塞队列,并执行阻塞操作。
3) 检测阻塞队列,当发现主机的阻塞时间已到时,从阻塞队列中删除该主机结点,且取消对该主机的阻塞。报警输出处理子程序的主要代码和相关全局变量定义如下:
int sockfd; // 监听Snort的报警消息int btime = (1 << 29) - 1; // 默认的阻塞时间struct _HNQueue AQ; // 报警队列struct _HNQueue BQ; // 阻塞队列// 线程同步所需mutex和条件变量pthread_mutex_t AQ_mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t BQ_mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t AQ_cond = PTHREAD_COND_INITIALIZER;pthread_cond_t BQ_cond = PTHREAD_COND_INITIALIZER;pthread_cond_t BQ_timedcond = PTHREAD_COND_INITIALIZER;pthread_t mtid; // 记录主线程IDInt main (int argc,char** argv){ struct sockaddr_un snortaddr; struct sockaddr_un bogus; Alertpkt alert; Packet *p = NULL; int recv; socklen_t len = sizeof (struct sockaddr_un); HNInfo *cur, *node, *pivot; pthread_t tidA, tidB; if(argc == 2 && !strcmp(argv[1],"-D")) daemon_init(); // 以后台方式运行//程序初始化工作,包括AQ、BQ的初始化,建立php请求监听端口和建立php请求处理线程池。 goInit(); // 捕获用户终止或kill消息并进行清理操作 signal(SIGINT, sig_int); signal(SIGTERM, sig_term); // 建立维护报警队列和阻塞队列的两个线程 pthread_create(&tidA, NULL, alertHandler, NULL); pthread_create(&tidB, NULL, blockHandler, NULL); mtid = pthread_self(); // 记录主线程ID// 使用UDP套接字接收Snort的报警消息 if ((sockfd = socket (AF_UNIX, SOCK_DGRAM, 0)) < 0) { perror ("socket"); exit (1); } unlink(UNSOCK_FILE); bzero (&snortaddr, sizeof (snortaddr)); snortaddr.sun_family = AF_UNIX; // 设置为Unix域套接字 strcpy (snortaddr.sun_path, UNSOCK_FILE); // 将path设为UNSOCK_FILE if (bind (sockfd, (struct sockaddr *) &snortaddr, sizeof (snortaddr)) < 0) { perror ("bind"); exit (1); }// 接收Snort的报警数据 while ((recv = recvfrom (sockfd, (void *) &alert, sizeof (alert), 0, (struct sockaddr *) &bogus, &len)) > 0) { p = ParsePacket(&alert); // 解析报警信息 if(p != NULL) { node = MakeNode(&alert, p); // 建立报警信息结点 if(node != NULL) { PushQueue(node); // 将报警信息加入报警队列 } free(p); p = NULL; } } exitClean(); // 清理工作 return 0;} |
主线程接收到报警数据并解析后,由MakeNode函数生成报警信息并由PushQueue函数加入报警队列:
HNInfo * MakeNode(Alertpkt* alert, Packet *p){ 分配新结点node; strcpy(node->blockIP , (char*) inet_ntoa (p->iph->ip_src)); // 解析源IP地址 strcpy(node->desIP, (char*) inet_ntoa(p->iph->ip_dst)); // 解析目的IP地址 node->timestamp = time(NULL); // 结点生成时间 node->blocktime = btime; // 阻塞时间 node->event_id = alert->event.event_id; // 事件ID号 strcpy(node->msg, alert->alertmsg); // 报警原因 // 解析报警协议及端口号 if (!(alert->val & NOPACKET_STRUCT)) if (p->iph && (p->tcph || p->udph || p->icmph)) { switch (p->iph->ip_proto) { case IPPROTO_TCP: strcpy(node->protocol, "TCP"); node->srcPort = ntohs (p->tcph->th_sport); node->desPort = ntohs (p->tcph->th_dport); break; case IPPROTO_UDP: strcpy(node->protocol, "UDP"); node->srcPort = ntohs (p->udph->uh_sport); node->desPort = ntohs (p->udph->uh_dport); break; case IPPROTO_ICMP: strcpy(node->protocol, "ICMP"); break; } } return node;}void PushQueue(HNInfo* node){ pthread_mutex_lock(&AQ_mutex); 加入报警信息结点; // 通知报警信息处理线程,有新结点加入 pthread_cond_signal(&AQ_cond); pthread_mutex_unlock(&AQ_mutex);} |
AlertHandler检测报警队列AQ,当队列中有结点时,则将其取出,与BQ中的结点比较,若BQ中没有相应主机的结点,则在BQ中添加该结点,否则只是修改BQ中相应结点的时间戳和报警事件原因。检测时使用条件变量,当AQ不为空时,再进行操作,否则程序进入睡眠,这比轮询的方式节省CPU资源。
void *alertHandler(void * vptr){ HNInfo* cur, *pivot, *tmp; char cmd[200]; while(1) { pthread_mutex_lock(&AQ_mutex); // 直到AQ不为空,线程被唤醒 while(AQ.count == 0) pthread_cond_wait(&AQ_cond, &AQ_mutex); cur = AQ.head; while(cur != NULL) { // 检验BQ中是否含相同主机结点 pthread_mutex_lock(&BQ_mutex); if(BQ.count) // BQ不为空 { cur = PopQueue(&AQ); if(找到相应结点)修改BQ中对应结点的值并释放cur; else{// 结点未找到,将其添加入BQ并添加防火墙规则 结点加入BQ; snprintf(cmd,sizeof(cmd),"/sbin/iptables -I INPUT -s %s -j DROP",cur->blockIP); system(cmd); // 添加防火墙规则 } } // BQ为空,只需添加结点、防火墙规则,并通知阻塞队列维护线程,BQ中有新结点产生 else { 结点加入BQ并添加防火墙规则 ; //通知阻塞队列维护线程 pthread_cond_signal(&BQ_cond); } pthread_mutex_unlock(&BQ_mutex); cur = AQ.head; } pthread_mutex_unlock(&AQ_mutex); }}// blockHandler检测阻塞主机队列,一旦某个主机阻塞时间已到,则从阻塞队列中删除,并// 将对应的阻塞主机放行void *blockHandler(void * vptr){ HNInfo* pivot, *cur; char cmd[200]; time_t min, tmp; while(1) { pthread_mutex_lock(&BQ_mutex); while(BQ.count == 0) // 若BQ为空,线程休眠 pthread_cond_wait(&BQ_cond, &BQ_mutex); pivot = BQ.head; cur = BQ.head; while(cur != NULL) { tmp = cur->timestamp + cur->blocktime ; if(cur == BQ.head)min = tmp; else min = min > tmp ? tmp : min; if(tmp <= time(NULL)) 时间到删除结点及防火墙规则; else { pivot = cur; cur = cur->next; } } // 当肯定没有主机阻塞时间到时,线程休眠。 struct timespec ts; ts.tv_sec = min; ts.tv_nsec = 0; pthread_cond_timedwait(&BQ_timedcond, &BQ_mutex, &ts); pthread_mutex_unlock(&BQ_mutex); }} |
PHP请求处理子程序
用户通过PHP页面对阻塞的主机进行管理操作,主要的操作有:
1) 实时查看阻塞主机信息;
2) 修改某阻塞主机的阻塞时间;
3) 将某主机从阻塞列表中删除。
此时,PHP服务器端程序需要和服务程序交互,由于交互过程比较简单,故采用问答方式,通信协议如下:
Client: command[&event_id[&blocktime]]
Server: cont&event_id&blockip×tamp&blocktime&msg&desIP&protocol&srcport&desport#
或over&result#
客户端发出的消息分为查询,删除,修改三种情况,对应command值为query、del、mod,当command为del时,需要填入event_id的值,而当command为mod时,需要加入event_id和blocktime的值,各个字段之间以&分隔。
服务器端的回应消息主要分两种,一种针对query,一种针对del和mod。对del和mod,只是简单返回执行结果,即over&result#,result为ok或err;而对query,需要返回所有阻塞主机的信息,每条信息放在一个'cont'开头的串中,各主机信息以'#'分隔,而各字段信息以'&'分隔。当所有的信息传送完毕时,发送一个over&result#消息。
PHP请求处理子程序主要代码如下:
int listenfd; // 处理PHP请求的监听端口pthread_t THREADPOOL[50]; // 处理php请求的线程池pthread_mutex_t WEB_mutex = PTHREAD_MUTEX_INITIALIZER;webHandler线程处理来自php的请求,并对阻塞队列进行相应操作。void* webHandler(void *vptr){ int connfd; // 与php通信端口 … while(1) { pthread_mutex_lock(&WEB_mutex); // 监听来自php的请求并建立通信连接 connfd = accept(listenfd, &cliaddr, &clilen); pthread_mutex_unlock(&WEB_mutex); memset(buf, 0, sizeof(buf)); if((n = read(connfd, buf, sizeof(buf))) == 0)continue; // 读取php请求 if(strlen(buf) == 0)continue; 解析客户端请求; if(查询BQ状态) { pthread_mutex_lock(&BQ_mutex); 读取并发送BQ信息; pthread_mutex_unlock(&BQ_mutex); } else if(删除或修改BQ中某主机信息) { pthread_mutex_lock(&BQ_mutex); 删除或修改主机信息和防火墙规则,并返回执行结果; pthread_mutex_unlock(&BQ_mutex); } else 发送出错消息; close(connfd); }} |
Php管理页面使用socket向服务程序发送请求,并显示相应结果。 PHP管理页面的查询和修改代码如下:
function DoQuery(&$errno){ $servSocket = OpenAlertSock(); // 打开通信端口 $buf = "query"; socket_write($servSocket, $buf, strlen($buf)); while ($out = socket_read($servSocket, 2048)) { $result = $result.$out; } socket_close($servSocket); 解析阻塞主机信息,并将其存入数组返回。}function DoModify($cmd){ $servSocket = OpenAlertSock(); socket_write($servSocket, $cmd, strlen($cmd)); // 发送删除或修改请求 while ($out = socket_read($servSocket, 2048)) { $result = $result.$out; } socket_close($servSocket); 解析操作结果并返回。} |
结束语
本文利用Snort和php构建了一个小型的网络防御系统,由于采用了独立的服务程序,防御系统对Snort和iptables的运行效率影响很小。
参考资料
|
|