隨著對網路安全需求的深入開發,基於網路的入侵檢測技術已經成為一個重要且有意思的研究方向。想學習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 實現抓包及檢測分析的主程式。
複製代碼 代碼如下:#!/usr/bin/perl
#
# Comments/suggestions to stardust at xfocus dot org
#
#
# $Id: perl-ids.pl,v 1.16 2004/03/04 21:51:12 stardust Exp $
#
# 引用所有相關的模組
use Net::PcapUtils;
use NetPacket::Ethernet qw(:strip);
use NetPacket::TCP;
use NetPacket::IP qw(:protos);
use NetPacket::SMB;
use NetPacket::FTP;
# 定義記錄檔名
$workingdir = "./";
$attacklog = "attack.log";
$monitorlog = "monitor.log";
# 以後台進程方式運行
daemon ();
sub daemon {
unless (fork) {
SniffLoop ();
exit 0;
}
exit 1;
}
# 抓包迴圈
sub SniffLoop {
# 進入工作目錄
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 => 'tcp or udp', DEV => 'eth0');
}
sub sniffit {
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 {}
}
sub SmbClientCheck {
my ($src_ip,$src_port,$dest_ip,$dst_port,$data) = @_;
# 調用SMB解碼模組解碼
$smb = NetPacket::SMB->decode($data);
# 如果解碼成功
if ($smb->{valid}) {
# 樣本檢測新近公布eeye的那個ASN.1解碼錯誤導致的堆破壞漏洞
# BID:9633,9635 CVEID:CAN-2003-0818 NSFOCUSID:6000
# 如果SMB命令是Session Setup AndX
if ($smb->{cmd} == 0x73) {
# 如果設定了Extended Security Negotiation位,表示有包裡有Security Blob
if ($smb->{flags2} & F2_EXTSECURINEG) {
# 用Regex匹配通常會在攻擊包裡出現的OID及引發錯誤的畸形資料串
# 由於不是從原理上檢測加之ASN.1編碼的靈活性,這樣的檢測會導致漏報
if (($smb->{bytecount} > 0) && ($smb->{bytes} =~ m/x06x06x2bx06x01x05x05x02.*[xa1x05x23x03x03x01x07 x84xffxffxff]/)) {
# 記入記錄檔
LogAlert ($src_ip,$src_port,$dest_ip,$dst_port,"ASN.1 malform encode attack!");
}
}
}
}
}
sub FtpClientCheck {
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:9751 NSFOCUSID: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") {
# 用Regex匹配引發溢出的參數串,這裡體現了正則
# 運算式的強大,用此匹配可以從原理上檢測到畸形參數串
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.4 MDTM command long timezone string overflow attack!");
}
}
}
}
}
# 記錄攻擊警示
sub LogAlert {
my ($src_ip,$src_port,$dest_ip,$dst_port,$message) = @_;
my $nowtime = localtime;
printf ATTACKLOG ("%s %s:%s -> %s:%s %s ",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message);
printf ("%s %s:%s -> %s:%s %s ",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message);
}
# 記錄監控資訊
sub LogMonitor {
my ($src_ip,$src_port,$dest_ip,$dst_port,$message) = @_;
my $nowtime = localtime;
printf MONITORLOG ("%s %s:%s -> %s:%s %s ",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message);
printf ("%s %s:%s -> %s:%s %s ",$nowtime,$src_ip,$src_port,$dest_ip,$dst_port,$message);
}
# INT訊號處理常式
sub HandleINT {
CleanUp ();
exit (0);
}
# TERM訊號處理常式
sub HandleTERM {
CleanUp ();
exit (0);
}
# 清理,主要工作是關閉檔案控制代碼
sub CleanUp {
close (ATTACKLOG); close (MONITORLOG);
}
(二)FTP.pm FTP協議解碼模組,抽取資料包裡的FTP命令及相應的參數,此檔案需要拷貝到NetPacket系列模組所在的目錄,通常是在/usr/lib/perl5/site_perl/5.x.x/NetPacket/
複製代碼 代碼如下:#
# NetPacket::FTP - Decode FTP packets
#
# Comments/suggestions to stardust at xfocus dot org
#
#
# $Id: FTP.pm,v 1.16 2004/03/03 l1:16:20 stardust Exp $
#
package NetPacket::FTP;
use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
use NetPacket;
my $myclass;
BEGIN {
$myclass = __PACKAGE__;
$VERSION = "0.01";
}
sub Version () { "$myclass v$VERSION" }
BEGIN {
@ISA = qw(Exporter NetPacket);
# Items to export into callers namespace by default
# (move infrequently used names to @EXPORT_OK below)
@EXPORT = qw(
);
# Other items we are prepared to export if requested
@EXPORT_OK = qw(
);
# Tags:
%EXPORT_TAGS = (
ALL => [@EXPORT, @EXPORT_OK],
);
}
#
# Decode the packet
#
# FTP協議文本參看RFC959,http://www.ietf.org/rfc/rfc0959.txt
# 常見的FTP命令
my @ftp_cmds = qw(ABOR ACCT ALLO APPE CDUP CWD DELE HELP LIST MKD MODE NLST
NOOP PASS PASV PORT PWD QUIT REIN REST RETR RMD RNFR RNTO
SITE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
XPWD XRMD LPRT LPSV ADAT AUTH CCC CONF ENC MIC PBSZ PROT
FEAT OPTS EPRT EPSV LANG MDTM MLSD MLST SIZE DIGT CLNT MACB
);
sub decode {
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;
}
sub splitcmd {
my ($cmdline,$parts) = @_;
# 去除行尾的斷行符號
chomp($cmdline);
# 用Regex抽取出命令名字和參數,既然效率不是考慮的主要問題就“毫無顧忌”地使用Regex,因為方便
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] eq uc($1)) {
$valid_cmd = 1;
last;
}
}
# 如果是合法的命令則返回給調用函數
if ($valid_cmd) {
${$parts}[0] = $1;
${$parts}[1] = $2;
return 1;
} else {
return 0;
}
} else {
return 0;
}
}
#
# Module initialisation
#
1;
# autoloaded methods go after the END token (&& pod) below
__END__
(三)SMB.pm 對SMB包頭結構的簡單解碼模組,此檔案需要拷貝到NetPacket系列模組所在的目錄,通常是在/usr/lib/perl5/site_perl/5.x.x/NetPacket/
複製代碼 代碼如下:#
# NetPacket::SMB - Decode SMB packets
#
# Comments/suggestions to stardust at xfocus dot org
#
#
# $Id: SMB.pm,v 1.16 2004/02/23 12:25:17 stardust Exp $
#
package NetPacket::SMB;
use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
use NetPacket;
my $myclass;
# SMB flags
use constant F2_LONGNAMEALLW => 0x0001;
use constant F2_EXTATTRIBUTE => 0x0002;
use constant F2_SECURITYSIGN => 0x0004;
use constant F2_LONGNAMEUSED => 0x0040;
use constant F2_EXTSECURINEG => 0x0800;
use constant F2_DONTRESOLDFS => 0x1000;
use constant F2_EXECONLYREAD => 0x2000;
use constant F2_ERRORCODTYPE => 0x4000;
use constant F2_UNICODSTRING => 0x8000;
use constant F_LOCKANDREAD => 0x01;
use constant F_RCVBUFFPOST => 0x02;
use constant F_CASESENSITV => 0x08;
use constant F_CANONICPATH => 0x10;
use constant F_OPLOCKSREQU => 0x20;
use constant F_NOTIFYONOPN => 0x40;
use constant F_REQUERESPON => 0x80;
BEGIN {
$myclass = __PACKAGE__;
$VERSION = "0.01";
}
sub Version () { "$myclass v$VERSION" }
BEGIN {
@ISA = qw(Exporter NetPacket);
# Items to export into callers namespace by default
# (move infrequently used names to @EXPORT_OK below)
@EXPORT = qw(F2_LONGNAMEALLW F2_EXTATTRIBUTE F2_SECURITYSIGN
F2_LONGNAMEUSED F2_EXTSECURINEG F2_DONTRESOLDFS
F2_EXECONLYREAD F2_ERRORCODTYPE F2_UNICODSTRING
F_LOCKANDREAD F_RCVBUFFPOST F_CASESENSITV
F_CANONICPATH F_OPLOCKSREQU F_NOTIFYONOPN
F_REQUERESPON
);
# Other items we are prepared to export if requested
@EXPORT_OK = qw(smb_strip
);
# Tags:
%EXPORT_TAGS = (
ALL => [@EXPORT, @EXPORT_OK],
strip => [qw(smb_strip)],
);
}
#
# Strip header from packet and return the data contained in it
#
undef &smb_strip;
*smb_strip = &strip;
# 剝除SMB頭的函數
sub strip {
my ($data) = @_;
my $smb_obj = NetPacket::SMB->decode($data);
return $smb_obj->{data};
}
#
# Decode the packet
#
sub decode {
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_mark ne "xffx53x4dx42") {
$self->{valid} = 0;
} else {
$self->{valid} = 1;
# Decode SMB packet
if (defined($data)) {
# 用PERL的unpack函數解碼32位元組長的SMB頭結構,頭結構可
# 參考 http://www.cs.uml.edu/~bill/cs592/cifs.chm
# 感謝小四(scz at nsfocus dot com)對於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)) {
# 解碼SMB結構下的wordcount位元組及bytecount位元組資料
my $wordbytes = $self->{wordcount} * 2;
($self->{wordcount},$self->{words},$self->{bytecount},$self->{bytes}) = unpack("C"."a"."$wordbytes"."va*",$self->{data});
} else {
($self->{wordcount},$self->{words}) = unpack("Ca*",$self->{data});
$self->{bytecount} = -1; $self->{bytes} = "";
}
}
}
}
# 返回對象
bless($self, $class);
return $self;
}
#
# Module initialisation
#
1;
# autoloaded methods go after the END token (&& pod) below
__END__