隨著對網絡安全需求的深入開發,基于網絡的入侵檢測技術已經成為一個重要且有意思的研究方向。想學習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)){