|
|
随着对网络安全需求的深入开发,基于网络的入侵检测技术已经成为一个重要且有意思的研究方向。想学习NIDS技术除了去读一些现成的资料和一些开源系统的源码,最好的办法莫过于自己去写一个NIDS程序,只有那样才能真正体会到一些NIDS的实现需求和设计妙处。
本质上说NIDS只是一种网络流量的分析工具,通过对网络流量的分析识别出一些已知或未知的攻击行为,一个最简单的NIDS完成的主要工作也就是抓包->协议解码->匹配,众所周知PERL是极其强大的脚本语言,尤其是它的字符串处理能力可以方便地实现对于网络流量中恶意特征进行匹配。当然PERL毕竟只是脚本语言,它的执行效率不允许用于真正大流量生产性环境,但PERL的简单易学及强大功能对于实现一个简单的NIDS达到学习的目的无疑是非常好的,下面我介绍一个用PERL实现的简单NIDS框架,我们将在Linux下实现它,在其他操作系统上类似。
PERL的一个强大特性就在于它海量的CPAN模块库,很多你想实现的功能都可以找到现成的模块,你所要做的只是安装上那些模块即可,关于PERL的模块及面向对象特性的管理和使用在这就不介绍了,请参看相关资料,比如O REILLY出版的《高级Perl编程》。在用PERL编写网络流量分析脚本之前,需要安装一些底层的抓包及基本的数据包解码模块,包括如下这些:
http://www.tcpdump.org/release/libpcap-0.8.1.tar.gz 底层基本的抓包库。
http://www.cpan.org/authors/id/T/TI/TIMPOTTER/Net-Pcap-0.04.tar.gz libpcap的PERL接口。
http://www.cpan.org/authors/id/T/TI/TIMPOTTER/Net-PcapUtils-0.01.tar.gz Net-Pcap模块的wrapper,包装Net-Pcap的函数,可以更方便地在PERL里调用抓包。
http://www.cpan.org/authors/id/T/TI/TIMPOTTER/NetPacket-0.03.tar.gz 用于基本的IP/TCP/UDP等包解码的模块,剥除各种协议头,抽取各个字段。
下面的代码演示了一个带有基本SMB和FTP协议解码模块的最简单NIDS框架,此程序实现最简单的NIDS功能,面向单包,不关心包的状态,不具备高级的商业NIDS产品诸如流重组,包状态及应用层协议的跟踪等功能。为了提高检测的准确性,与Snort直接匹配数据区不同的是,这个脚本实现了两个应用层协议:SMB、FTP的简单解码,解码完全是面向NIDS的需要,代码也没有经过仔细的测试可能存在问题,有什么建议可联系我。
perl-ids.pl 实现抓包及检测分析的主程序。 ------------------------------8<---------------------------------------- #!/usr/bin/perl # #Comments/suggestionstostardustatxfocusdotorg # # #$Id:perl-ids.pl,v1.162004/03/0421:51:12stardustExp$ #
#引用所有相关的模块 useNet::PcapUtils; useNetPacket::Ethernetqw(:strip); useNetPacket::TCP; useNetPacket::IPqw(:protos); useNetPacket::SMB; useNetPacket::FTP;
#定义日志文件名 $workingdir="./"; $attacklog="attack.log"; $monitorlog="monitor.log";
#以后台进程方式运行 daemon();
subdaemon{ unless(fork){ SniffLoop(); exit0; } exit1; }
#抓包循环 subSniffLoop{
#进入工作目录 chdir("$workingdir"); #打开日志文件 open(ATTACKLOG,">>$attacklog"); open(MONITORLOG,">>$monitorlog"); #设置文件读写为非缓冲模式 select(ATTACKLOG);$|++;select(MONITORLOG);$|++;select(STDOUT);$|++;
#设置信号处理函数,因为程序运行于后台,退出时需要利用信号处理函数做些清理工作 $SIG{"INT"}= HandleINT ; $SIG{"TERM"}= HandleTERM ;
#进入抓包回调函数 Net::PcapUtils::loop(&sniffit,SNAPLEN=>1800,Promisc=>1,FILTER=> tcporudp ,DEV=> eth0 ); }
subsniffit{ my($args,$header,$packet)=@_; #解码IP包 $ip=NetPacket::IP->decode(eth_strip($packet));
#TCP协议 if($ip->{proto}==IP_PROTO_TCP){ #解码TCP包 $tcp=NetPacket::TCP->decode($ip->{data});
#检查来自SMB客户端的包 if(($tcp->{dest_port}==139)||($tcp->{dest_port}==445)){ #如果目的端口是139或445,认为是SMB协议包,做相应的检查 SmbClientCheck($ip->{src_ip},$tcp->{src_port},$ip->{dest_ip},$tcp->{dest_port},$tcp->{data});
}elsif($tcp->{dest_port}==21){ #如果目的端口是21,认为是FTP协议,做相应的检查 FtpClientCheck($ip->{src_ip},$tcp->{src_port},$ip->{dest_ip},$tcp->{dest_port},$tcp->{data});
}else{} #UDP协议 }elsif($ip->{proto}==IP_PROTO_UDP){ }else{} }
subSmbClientCheck{ my($src_ip,$src_port,$dest_ip,$dst_port,$data)=@_;
#调用SMB解码模块解码 $smb=NetPacket::SMB->decode($data);
#如果解码成功 if($smb->{valid}){ #示例检测新近公布eeye的那个ASN.1解码错误导致的堆破坏漏洞 #BID:9633,9635CVEID:CAN-2003-0818NSFOCUSID:6000
#如果SMB命令是SessionSetupAndX if($smb->{cmd}==0x73){ #如果设置了ExtendedSecurityNegotiation位,表示有包里有SecurityBlob if($smb->{flags2}&F2_EXTSECURINEG){ #用正则表达式匹配通常会在攻击包里出现的OID及引发错误的畸形数据串 #由于不是从原理上检测加之ASN.1编码的灵活性,这样的检测会导致漏报 if(($smb->{bytecount}>0)&&($smb->{bytes}=~m/x06x06x2bx06x01x05x05x02.*[xa1x05x23x03x03x01x07|x84xffxffxff]/)){ #记入日志文件 LogAlert($src_ip,$src_port,$dest_ip,$dst_port,"ASN.1malformencodeattack!"); } } } } }
subFtpClientCheck{ my($src_ip,$src_port,$dest_ip,$dst_port,$data)=@_;
#调用FTP解码模块解码 $ftp=NetPacket::FTP->decode($data);
#如果解码成功 if($ftp->{valid}){ #示例检测新近公布的Serv-U<5.0.0.4版FTP服务器MDTM命令溢出攻击 #BID:9751NSFOCUSID:6078
#遍历从数据包里解码出来的FTP命令及其参数 for(my$i=1;$i<=$ftp->{cmdcount};$i++){ my$cmd="cmd"."$i"; my$para="para"."$i"; #如果FTP命令是MDTM if(uc($ftp->{$cmd})eq"MDTM"){ #用正则表达式匹配引发溢出的参数串,这里体现了正则 #表达式的强大,用此匹配可以从原理上检测到畸形参数串 if($ftp->{$para}=~m/d{14}[+|-]S{5,}s+S{1,}/){ LogAlert($src_ip,$src_port,$dest_ip,$dst_port,"Serv-U<v5.0.0.4MDTMcommandlongtimezonestringoverflowattack!"); } } } } }
#记录攻击告警 subLogAlert{ my($src_ip,$src_port,$dest_ip,$dst_port,$message)=@_;
my$nowtime=localtime; printfATTACKLOG("%st%s:%s->%s:%st%sn",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message); printf("%st%s:%s->%s:%st%sn",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message); }
#记录监控信息 subLogMonitor{ my($src_ip,$src_port,$dest_ip,$dst_port,$message)=@_;
my$nowtime=localtime; printfMONITORLOG("%st%s:%s->%s:%st%sn",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message); printf("%st%s:%s->%s:%st%sn",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message); }
#INT信号处理例程 subHandleINT{
CleanUp(); exit(0); }
#TERM信号处理例程 subHandleTERM{
CleanUp(); exit(0); }
#清理,主要工作是关闭文件句柄 subCleanUp{ close(ATTACKLOG);close(MONITORLOG); } ------------------------------8<----------------------------------------
FTP.pm FTP协议解码模块,抽取数据包里的FTP命令及相应的参数,此文件需要拷贝到NetPacket系列模块所在的目录,通常是在/usr/lib/perl5/site_perl/5.x.x/NetPacket/ ------------------------------8<---------------------------------------- # #NetPacket::FTP-DecodeFTPpackets # #Comments/suggestionstostardustatxfocusdotorg # # #$Id:FTP.pm,v1.162004/03/03l1:16:20stardustExp$ #
packageNetPacket::FTP;
usestrict; usevarsqw($VERSION@ISA@EXPORT@EXPORT_OK%EXPORT_TAGS); useNetPacket;
my$myclass;
BEGIN{ $myclass=__PACKAGE__; $VERSION="0.01"; } subVersion(){"$myclassv$VERSION"}
BEGIN{ @ISA=qw(ExporterNetPacket);
#Itemstoexportintocallersnamespacebydefault #(moveinfrequentlyusednamesto@EXPORT_OKbelow)
@EXPORT=qw( );
#Otheritemswearepreparedtoexportifrequested
@EXPORT_OK=qw( );
#Tags:
%EXPORT_TAGS=( ALL=>[@EXPORT,@EXPORT_OK], );
}
# #Decodethepacket # #FTP协议文本参看RFC959,http://www.ietf.org/rfc/rfc0959.txt
#常见的FTP命令 my@ftp_cmds=qw(ABORACCTALLOAPPECDUPCWDDELEHELPLISTMKDMODENLST NOOPPASSPASVPORTPWDQUITREINRESTRETRRMDRNFRRNTO SITESMNTSTATSTORSTOUSTRUSYSTTYPEUSERXCUPXCWDXMKD XPWDXRMDLPRTLPSVADATAUTHCCCCONFENCMICPBSZPROT FEATOPTSEPRTEPSVLANGMDTMMLSDMLSTSIZEDIGTCLNTMACB );
subdecode{ my$class=shift; my($data)=@_; my$self={};
my$cmdhead=0; my$cmdtail=0; my@parts=(); my$cmdcount=0; my$returnindex=0; my$data_len=length($data);
#如果数据长度过短则不处理 if($data_len>=4){ #一个包里的FTP命令个数 $self->{cmdcount}=0; #搜索回车,之前认为是一个命令行,需要注意的是一个包里可能包含多个FTP命令 while((($returnindex=index($data,"x0a",$cmdhead))>=0)||(($returnindex<0)&&(($data_len-$cmdhead)>=4))){ #调整一个命令行串尾指针 if($returnindex<0){ $cmdtail=$data_len-1; }else{ $cmdtail=$returnindex; }
if((my$cmdlen=($cmdtail-$cmdhead+1))>=4){ #取出命令行串 my$cmdline=substr($data,$cmdhead,$cmdlen); #从命令行里拆分出命令名和它的参数串 if(splitcmd($cmdline,@parts)){ $self->{cmdcount}++; my$cmdindex="cmd"."$self->{cmdcount}"; my$paraindex="para"."$self->{cmdcount}"; #记录到要返回到主程序的对象 $self->{$cmdindex}=$parts[0]; $self->{$paraindex}=$parts[1]; } } #调整命令行串头指针 $cmdhead=$cmdtail+1; }
#如果命令个数大于0,则说明解码是有效的 if($self->{cmdcount}==0){ $self->{valid}=0; }else{ $self->{valid}=1; }
}else{ $self->{valid}=0; }
#返回对象
bless($self,$class); return$self; }
subsplitcmd{ my($cmdline,$parts)=@_;
#去除行尾的回车 chomp($cmdline); #用正则表达式抽取出命令名字和参数,既然效率不是考虑的主要问题就“毫无顾忌”地使用正则表达式,因为方便 if($cmdline=~m/^s*([a-zA-Z]{3,4})s+(.*)/){ my$valid_cmd=0; #检查抽出来的命令名字是否是一个已知的合法FTP命令 for(my$i=0;$i<@ftp_cmds;$i++){ if($ftp_cmds[$i]equc($1)){ $valid_cmd=1; last; } } #如果是合法的命令则返回给调用函数 if($valid_cmd){ ${$parts}[0]=$1; ${$parts}[1]=$2; return1; }else{ return0; }
}else{ return0; } }
# #Moduleinitialisation #
1;
#autoloadedmethodsgoaftertheENDtoken(&&pod)below
__END__ ------------------------------8<----------------------------------------
SMB.pm 对SMB包头结构的简单解码模块,此文件需要拷贝到NetPacket系列模块所在的目录,通常是在/usr/lib/perl5/site_perl/5.x.x/NetPacket/ ------------------------------8<---------------------------------------- # #NetPacket::SMB-DecodeSMBpackets # #Comments/suggestionstostardustatxfocusdotorg # # #$Id:SMB.pm,v1.162004/02/2312:25:17stardustExp$ #
packageNetPacket::SMB;
usestrict; usevarsqw($VERSION@ISA@EXPORT@EXPORT_OK%EXPORT_TAGS); useNetPacket;
my$myclass;
#SMBflags
useconstantF2_LONGNAMEALLW=>0x0001; useconstantF2_EXTATTRIBUTE=>0x0002; useconstantF2_SECURITYSIGN=>0x0004; useconstantF2_LONGNAMEUSED=>0x0040; useconstantF2_EXTSECURINEG=>0x0800; useconstantF2_DONTRESOLDFS=>0x1000; useconstantF2_EXECONLYREAD=>0x2000; useconstantF2_ERRORCODTYPE=>0x4000; useconstantF2_UNICODSTRING=>0x8000;
useconstantF_LOCKANDREAD=>0x01; useconstantF_RCVBUFFPOST=>0x02; useconstantF_CASESENSITV=>0x08; useconstantF_CANONICPATH=>0x10; useconstantF_OPLOCKSREQU=>0x20; useconstantF_NOTIFYONOPN=>0x40; useconstantF_REQUERESPON=>0x80;
BEGIN{ $myclass=__PACKAGE__; $VERSION="0.01"; } subVersion(){"$myclassv$VERSION"}
BEGIN{ @ISA=qw(ExporterNetPacket);
#Itemstoexportintocallersnamespacebydefault #(moveinfrequentlyusednamesto@EXPORT_OKbelow)
@EXPORT=qw(F2_LONGNAMEALLWF2_EXTATTRIBUTEF2_SECURITYSIGN F2_LONGNAMEUSEDF2_EXTSECURINEGF2_DONTRESOLDFS F2_EXECONLYREADF2_ERRORCODTYPEF2_UNICODSTRING F_LOCKANDREADF_RCVBUFFPOSTF_CASESENSITV F_CANONICPATHF_OPLOCKSREQUF_NOTIFYONOPN F_REQUERESPON );
#Otheritemswearepreparedtoexportifrequested
@EXPORT_OK=qw(smb_strip );
#Tags:
%EXPORT_TAGS=( ALL=>[@EXPORT,@EXPORT_OK], strip=>[qw(smb_strip)], );
}
# #Stripheaderfrompacketandreturnthedatacontainedinit #
undef&smb_strip; *smb_strip=&strip;
#剥除SMB头的函数 substrip{ my($data)=@_;
my$smb_obj=NetPacket::SMB->decode($data); return$smb_obj->{data}; }
# #Decodethepacket #
subdecode{ my$class=shift; my($data)=@_; my$self={}; my$data_len=0; my$temp="";
$data_len=length($data); #如果数据区长度小于39字节(4+32+3),则认为不是一个可解码的SMB包 if($data_len<39){
$self->{valid}=0;
}else{ #取SMB的标志串 my$smb_mark=substr($data,4,4);
#是否符合标志串 if($smb_markne"xffx53x4dx42"){ $self->{valid}=0; }else{ $self->{valid}=1;
#DecodeSMBpacket
if(defined($data)){ #用PERL的unpack函数解码32字节长的SMB头结构,头结构可 #参考http://www.cs.uml.edu/~bill/cs592/cifs.chm #感谢小四(sczatnsfocusdotcom)对于SMB头结构中字段字节序的提醒 ($self->{nbt_type},$self->{nbt_flag},$self->{nbt_len}, $self->{mark},$self->{cmd},$self->{status}, $self->{flags},$self->{flags2},$self->{ext}, $self->{ext2},$self->{ext3},$self->{tid}, $self->{pid},$self->{uid},$self->{mid}, $self->{data})=unpack("CCna4CVCvVVVvvvva*",$data); ($self->{wordcount},$temp)=unpack("Ca*",$self->{data});
if((36+1+$self->{wordcount}*2)<=($data_len-2)){
|