一、簡介
libnids的英文意思是 Network Intrusion Detect System library,即網絡入侵監測系統函數庫。它是在前面介紹的兩種C函數接口庫libnet和libpcap的基礎上開發的,封裝了開發NIDS所需的許 多通用型函數。linids提供的接口函數監視流經本地的所有網絡通信,檢查數據包等。除此之外,還具有重組TCP數據段、處理IP分片包和監測TCP端 口掃描的功能。利用libnids接口函數庫,NIDS開發者不需要再編寫底層的網絡處理代碼,只需專注於NIDS本身功能的實現即可。
二、IP分片數據包
為了使libnids能接收所有的IP數據包(包括分片包、畸形包等),程序員需要定義如下的回調函數:
void ip_frag_func(struct ip * a_packet)
在調用nids_init()函數初始化后,使用nids的函數進行注冊:
nids_register_ip_frag(ip_frag_func);
這樣回調函數ip_frag_func會在適當的時候由libnids調用,參數a_packet指針將指向接收到的數據報。
類似地,如果僅接收目標主機會接受的數據包(如非碎片包、重組包或頭部校驗正確的數據包等),需要定義如下回調函數:
void ip_func(struct ip * a_packet)
然后注冊:
nids_register_ip(ip_func);
三、TCP數據流重組
要接收TCP流在交換的數據,必須定義如下回調函數:
void tcp_callback(struct tcp_stream * ns, void ** param)
tcp_stream結構提供了一個TCP連接的所有信息。例如,它包含了客戶端與服務器端的half_stream結構。下文會對該結構的字段進行解釋。
tcp_stream結構有一個名為nids_state的字段。此字段的數值將決定tcp_callback的操作。
(a) ns->nids_state==NIDS_JUST_EST時,ns表示一個剛剛建立的連接。
tcp_callback可以據此決定是否對該連接的后續數據進行檢查。如
需要檢查,tcp_callback回調函數將通知libnids它希望接收哪些
數據(如到客戶端的數據、到服務器端的數據、到客戶端的緊急數
據或到服務器端的緊急數據等),然后返回。
(b) ns->nids_state==NIDS_DATA時,表示ns連接接收到新的數據。
half_stream結構中的緩沖區用於存放這些數據。
(c) nids_state字段為其它數值(NIDS_CLOSE、NIDS_RESET、
NIDS_TIMEOUT)時,表示該連接已經關閉了。tcp_callback函數應
釋放相關資源。
四、一個簡單的實例
下面的源代碼是一個非常簡單的程序,它將libnids捕獲的所有TCP連接交換的數據輸出顯示到標准輸出設備上。
-----------------------BEGINING OF CODE--------------------------------
#include "nids.h"
#include <string.h>
#include <stdio.h>
extern char * inet_ntoa(unsigned long);
// tuple4結構包含了TCP連接兩端的IP地址和端口,以下函數將它們轉換為字符串
// 格式,如10.0.0.1,1024, 10.0.0.2,23
char *
adres (struct tuple4 addr)
{
static char buf[256];
strcpy (buf, inet_ntoa (addr.saddr));
sprintf (buf + strlen (buf), ",%i,", addr.source);
strcat (buf, inet_ntoa (addr.daddr));
sprintf (buf + strlen (buf), ",%i", addr.dest);
return buf;
}
void
tcp_callback (struct tcp_stream *a_tcp, void ** this_time_not_needed)
{
char buf[1024];
strcpy (buf, adres (a_tcp->addr)); // we put conn params into buf
if (a_tcp->nids_state == NIDS_JUST_EST)
{
// a_tcp所定義的連接已經建立。此處可視程序需要添加額外
// 的判斷處理。如if (a_tcp->addr.dest != 23) return;表
// 示不處理目標端口為23的數據包。
// 本例需要處理(顯示)所有數據包,故:
a_tcp->client.collect++; // 需要處理客戶端接收的數據
a_tcp->server.collect++; // 和服務器端接收的數據
a_tcp->server.collect_urg++; // 需要處理服務器端接收的緊急數據
#ifdef WE_WANT_URGENT_DATA_RECEIVED_BY_A_CLIENT
a_tcp->client.collect_urg++; // 需要處理客戶端接收的緊急數據
// (打開編譯選項才有效)
#endif
fprintf (stderr, "%s established\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_CLOSE)
{
// TCP連接正常關閉
fprintf (stderr, "%s closing\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_RESET)
{
// TCP連接因RST數據包而關閉
fprintf (stderr, "%s reset\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_DATA)
{
// 接收到新數據,下面判斷決定是否顯示
struct half_stream *hlf;
if (a_tcp->server.count_new_urg)
{
// 服務器端接收的緊急數據
strcat(buf,"(urgent->)");
buf[strlen(buf)+1]=0;
buf[strlen(buf)]=a_tcp->server.urgdata;
write(1,buf,strlen(buf));
return;
}
#ifdef WE_WANT_URGENT_DATA_RECEIVED_BY_A_CLIENT
if (a_tcp->client.count_new_urg)
{
// 客戶端接收的緊急數據
strcat(buf,"(urgent->)");
buf[strlen(buf)+1]=0;
buf[strlen(buf)]=a_tcp->server.urgdata;
write(1,buf,strlen(buf));
return;
}
#endif
if (a_tcp->client.count_new)
{
// 客戶端接收的數據
hlf = &a_tcp->client; // 准備顯示客戶端接收的數據
strcat (buf, "(<-)"); // 指示數據流方向
}
else
{
hlf = &a_tcp->server; // 准備顯示服務器端接收的數據
strcat (buf, "(->)"); // 指示數據流方向
}
fprintf(stderr,"%s",buf); // 首先輸出顯示連接雙方的IP地址、端口
// 和數據流方向
write(2,hlf->data,hlf->count_new); // 輸出顯示接收到的新數據
}
return ;
}
int
main ()
{
// 此處可自定義libnids的全局變量,如:
// nids_params.n_hosts=256;
if (!nids_init () )
{
fprintf(stderr,"%s\n",nids_errbuf);
exit(1);
}
nids_register_tcp (tcp_callback);
nids_run ();
// NOT REACHED
return 0;
}
---------------------------END OF CODE------------------------------------
五、libnids的數據結構及接口函數
libnids庫的所有數據結構及接口函數都在"nids.h"頭文件中聲明。
struct tuple4 // TCP連接參數
{
unsigned short source,dest; // 客戶端和服務器端的端口號
unsigned long saddr,daddr; // 客戶端和服務器端的IP地址
};
struct half_stream // TCP連接一端的數據結構
{
char state; // 套接字狀態(如TCP_ESTABLISHED)
char collect; // 如果大於0,則保存其數據到緩沖區中,否則忽略
char collect_urg; // 如果大於0,則保存緊急數據,否則忽略
char * data; // 正常數據的緩沖區
unsigned char urgdata; // 緊急數據緩沖區
int count; // 自從連接建立以來保存到"data"緩沖區的數據字節
// 數總和
int offset; // 保存到"data"緩沖區的首字節數據偏移量
int count_new; // 最近一次接收到的數據字節數;如果為0,則無數
// 到達
char count_new_urg; // 如果非0,表示有新的緊急數據到達
... // libnids庫使用的輔助字段
};
struct tcp_stream
{
struct tuple4 addr; // TCP連接參數(saddr, daddr, sport, dport)
char nids_state; // TCP連接的邏輯狀態
struct half_stream client,server; // 描述客戶端與服務器端的數據結構
... // libnids庫使用的輔助字段
};
在上面的實例程序中,回調函數tcp_callback輸出顯示hlf->data緩沖區中的數據到標准輸出設備上。這些數據在 tcp_callback函數返回后,由libnids自動釋放這些數據所占用的內存空間。同時,hlf->offset字段將增加被丟棄數據的字 節數,而新接收到的數據則存放到"data"緩沖區的起始處。
如果在其它應用中不進行如上例的操作(例如,數據處理過程至少需要N個字節的輸入數據,而libnids只接收到的數據字節數count_new<N),則需要在tcp_callback函數返回前調用如下函數:
void nids_discard(struct tcp_stream * a_tcp, int num_bytes)
此時,當回調函數tcp_callback返回后linids將"data"緩沖區的前num_bytes字節數據,同時計算調整offset字段的數值,並將剩余數據移動到緩沖區的起始處。
如果始終不調用nids_discard()函數(如上面實例),hlf->data緩沖區中將包含hlf->count_new字節數據。 通常情況下,在hlf->data緩沖區中的數據字節數等於hlf->count - hlf->offset。
有了nids_discard()函數,程序員就不必拷貝接收到的數據到另外的緩沖區中,hlf->data緩沖區將總是盡可能保存足夠的數據。然 后,有時會有保留數據包特定數據的需要。例如,我們希望能監測到針對wu-ftpd服務器的"CWD"溢出攻擊,就需要跟蹤檢查ftp客戶端發送的 "CWD"命令。此時就需要tcp_callback回調函數具有第二個參數了。此參數是某TCP連接私有數據的指針。處理過程如下:
void
tcp_callback_2 (struct tcp_stream * a_tcp, struct conn_param **ptr)
{
if (a_tcp->nids_state==NIDS_JUST_EST)
{
struct conn_param * a_conn;
if the connection is uninteresting, return;
a_conn=malloc of some data structure
init of a_conn
*ptr=a_conn // this value will be passed to tcp_callback_2 in future
// calls
increase some of "collect" fields
return;
}
if (a_tcp->nids_state==NIDS_DATA)
{
struct conn_param *current_conn_param=*ptr;
using current_conn_param and the newly received data from the net
we search for attack signatures, possibly modyfying
current_conn_param
return ;
}
...
}
nids_register_tcp和nids_register_ip*函數可被任意次調用。在同一個TCP連接中使用兩種不同的回調函數是允許的。
libnids庫定義了一個全局變量結構nids_params,其聲明如下:
struct nids_prm
{
int n_tcp_streams; // 存放tcp_stream結構的hash表大小。
// 缺省值:1024
int n_hosts; // 存放IP分片信息的hash表大小
// 缺省值:256
char * device; // libnids監聽的接口設備名
// 缺省值 == NULL,即由pcap_lookupdev函數確定
int sk_buff_size; // (Linux內核)sk_buff結構大小
// 缺省值:168
int dev_addon; // sk_buff為網絡接口保留的字節數
// 如果dev_addon==-1,則由nids_init函數確定
// 缺省值:-1
void (*syslog)(); // 日志函數指針
int syslog_level; // 如果nids_params.syslog==nids_syslog,則此字段值
// 將確定日志等級loglevel
// 缺省值:LOG_ALERT
int scan_num_hosts;// 存放端口掃描信息的hash表大小。
// 如果為0,則關閉端口掃描監測功能。
// 缺省值:256
int scan_num_ports;// 來自同一IP地址所掃描的TCP端口數上限
// 缺省值:10
int scan_delay; // 在兩次端口掃描中的間隔時間上限(毫秒)
// 缺省值:3000
void (*no_mem)(); // 內存不足時被調用,此時應終止當前進程
int (*ip_filter)(struct ip*); // 當接收到一個IP數據包時調用。如返回值
// 非零,則處理該數據包,否則忽略。
// 缺省為(nids_ip_filter)且總返回1
char *pcap_filter; // 傳遞給pcap過濾器的字符串。
// 缺省值:NULL
} nids_params;
nids_params的syslog字段缺省時指向nids_syslog函數,聲明如下:
void nids_syslog (int type, int errnum, struct ip *iph, void *data);
nids_params.syslog函數用於記錄異常情況,如端口掃描企圖,無效TCP頭標志等。該字段應指向自定義的日志處理函數。nids_syslog()僅作為一個例子。nids_syslog()函數向系統守護服務syslogd發送日志消息。
使用nids_run有一個缺陷:應用程序將完全由數據包驅動(運行)。有時需要在沒有數據包到達時也能處理一些任務,則作為nids_run()函數的替代,程序員可使用如下函數:
int nids_next()
此函數將調用pcap_next()函數(而不是pcap_loop()函數)。(詳細資料請參閱《網絡安全工具開發函數庫介紹之二 ——libpcap》。) nids_next()函數成功時返回1,出錯時返回0,且nids_errbuf緩沖區存放相應錯誤消息。
典型地,當使用nids_next()函數時,應用程序調用I/O復用函數select()阻塞,監聽套接字fd在“讀”描述字集合fd_set中設置。該套接字可通過如下函數獲得:
int nids_getfd()
成功時返回一個文件描述字,出錯時返回-1,且nids_errbuf緩沖區存放相應錯誤消息。
---[[ libnids應用實例 ]]----------------------------------
1、nids_next()函數的應用
============================ cut here ============================
/*
This is an example how one can use nids_getfd() and nids_next() functions.
You can replace printall.c's function main with this file.
*/
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int
main ()
{
// here we can alter libnids params, for instance:
// nids_params.n_hosts=256;
int fd;
int time = 0;
fd_set rset;
struct timeval tv;
if (!nids_init ())
{
fprintf(stderr,"%s\n",nids_errbuf);
exit(1);
}
nids_register_tcp (tcp_callback);
fd = nids_getfd ();
for (;;)
{
tv.tv_sec = 1;
tv.tv_usec = 0;
FD_ZERO (&rset);
FD_SET (fd, &rset);
// add any other fd we need to take care of
if (select (fd + 1, &rset, 0, 0, &tv))
{
if (FD_ISSET(fd,&rset) // need to test it if there are other
// fd in rset
if (!nids_next ()) break;
}
else
fprintf (stderr, "%i ", time++);
}
return 0;
}
============================ cut here ============================
2、Simple sniffer
============================ cut here ============================
/*
Copyright (c) 1999 Rafal Wojtczuk <nergal@avet.com.pl>. All rights reserved.
See the file COPYING for license details.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <fcntl.h>
#include "nids.h"
#define LOG_MAX 100
#define SZLACZEK "\n--------------------------------------------------\n"
#define int_ntoa(x) inet_ntoa(*((struct in_addr *)&x))
char *
adres (struct tuple4 addr)
{
static char buf[256];
strcpy (buf, int_ntoa (addr.saddr));
sprintf (buf + strlen (buf), ",%i,", addr.source);
strcat (buf, int_ntoa (addr.daddr));
sprintf (buf + strlen (buf), ",%i : ", addr.dest);
return buf;
}
int logfd;
void
do_log (char *adres_txt, char *data, int ile)
{
write (logfd, adres_txt, strlen (adres_txt));
write (logfd, data, ile);
write (logfd, SZLACZEK, strlen (SZLACZEK));
}
void
sniff_callback (struct tcp_stream *a_tcp, void **this_time_not_needed)
{
int dest;
if (a_tcp->nids_state == NIDS_JUST_EST)
{
dest = a_tcp->addr.dest;
if (dest == 21 || dest == 23 || dest == 110 || dest == 143 || dest == 513)
a_tcp->server.collect++;
return;
}
if (a_tcp->nids_state != NIDS_DATA)
{
// seems the stream is closing, log as much as possible
do_log (adres (a_tcp->addr), a_tcp->server.data,
a_tcp->server.count - a_tcp->server.offset);
return;
}
if (a_tcp->server.count - a_tcp->server.offset < LOG_MAX)
{
// we haven't got enough data yet; keep all of it
nids_discard (a_tcp, 0);
return;
}
// enough data
do_log (adres (a_tcp->addr), a_tcp->server.data, LOG_MAX);
// Now procedure sniff_callback doesn't want to see this stream anymore.
// So, we decrease all the "collect" fields we have previously increased.
// If there were other callbacks following a_tcp stream, they would still
// receive data
a_tcp->server.collect--;
}
int
main ()
{
logfd = open ("./logfile", O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (logfd < 0)
{
perror ("opening ./logfile:");
exit (1);
}
if (!nids_init ())
{
fprintf (stderr, "%s\n", nids_errbuf);
exit (1);
}
nids_register_tcp (sniff_callback);
nids_run ();
return 0;
}
============================ cut here ============================
3、Wu-FTPd overflow attack detector
============================ cut here ============================
/*
Copyright (c) 1999 Rafal Wojtczuk <nergal@avet.com.pl>. All rights reserved.
See the file COPYING for license details.
*/
/*
This code attempts to detect attack against imapd (AUTHENTICATE hole) and
wuftpd (creation of deep directory). This code is to ilustrate use of libnids;
in order to improve readability, some simplifications were made, which enables
an attacker to bypass this code (note, the below routines should be improved,
not libnids)
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include "nids.h"
#define int_ntoa(x) inet_ntoa(*((struct in_addr *)&x))
char *
adres (struct tuple4 addr)
{
static char buf[256];
strcpy (buf, int_ntoa (addr.saddr));
sprintf (buf + strlen (buf), ",%i,", addr.source);
strcat (buf, int_ntoa (addr.daddr));
sprintf (buf + strlen (buf), ",%i", addr.dest);
return buf;
}
/*
if we find a pattern AUTHENTICATE {an_int} in data stream sent to an imap
server, where an_int >1024, it means an buffer overflow attempt. We kill the
connection.
*/
#define PATTERN "AUTHENTICATE {"
#define PATLEN strlen(PATTERN)
void
detect_imap (struct tcp_stream *a_tcp)
{
char numbuf[30];
int i, j, datalen, numberlen;
struct half_stream *hlf;
if (a_tcp->nids_state == NIDS_JUST_EST)
{
if (a_tcp->addr.dest == 143)
{
a_tcp->server.collect++;
return;
}
else
return;
}
if (a_tcp->nids_state != NIDS_DATA)
return;
hlf = &a_tcp->server;
datalen = hlf->count - hlf->offset;
if (datalen < PATLEN)
{
// we have too small amount of data to work on. Keep all data in buffer.
nids_discard (a_tcp, 0);
return;
}
for (i = 0; i <= datalen - PATLEN; i++)
if (!memcmp (PATTERN, hlf->data + i, PATLEN)) //searching for a pattern
break;
if (i > datalen - PATLEN)
{
// retain PATLEN bytes in buffer
nids_discard (a_tcp, datalen - PATLEN);
return;
}
for (j = i + PATLEN; j < datalen; j++) // searching for a closing '}'
if (*(hlf->data + j) == '}')
break;
if (j > datalen)
{
if (datalen > 20)
{
//number too long, perhaps we should log it, too
}
return;
}
numberlen = j - i - PATLEN;
memcpy (numbuf, hlf->data + i + PATLEN, numberlen); //numbuf contains
// AUTH argument
numbuf[numberlen] = 0;
if (atoi (numbuf) > 1024)
{
// notify admin
syslog(nids_params.syslog_level,
"Imapd exploit attempt, connection %s\n",adres(a_tcp->addr));
// kill the connection
nids_killtcp (a_tcp);
}
nids_discard (a_tcp, datalen - PATLEN);
return;
}
// auxiliary structure, needed to keep current dir of ftpd daemon
struct supp
{
char *currdir;
int last_newline;
};
// the below function adds "elem" string to "path" string, taking care of
// ".." and multiple '/'. If the resulting path is longer than 768,
// return value is 1, otherwise 0
int
add_to_path (char *path, char *elem, int len)
{
int plen;
char * ptr;
if (len > 768)
return 1;
if (len == 2 && elem[0] == '.' && elem[1] == '.')
{
ptr = rindex (path, '/');
if (ptr != path)
*ptr = 0;
}
else if (len > 0)
{
plen = strlen (path);
if (plen + len + 1 > 768)
return 1;
if (plen==1)
{
strncpy(path+1,elem,len);
path[1+len]=0;
}
else
{
path[plen] = '/';
strncpy (path + plen + 1, elem, len);
path[plen + 1 + len] = 0;
}
}
return 0;
}
void
do_detect_ftp (struct tcp_stream *a_tcp, struct supp **param_ptr)
{
struct supp *p = *param_ptr;
int index = p->last_newline + 1;
char *buf = a_tcp->server.data;
int offset = a_tcp->server.offset;
int n_bytes = a_tcp->server.count - offset;
int path_index, pi2, index2, remcaret;
for (;;)
{
index2 = index;
while (index2 - offset < n_bytes && buf[index2 - offset] != '\n')
index2++;
if (index2 - offset >= n_bytes)
break;
if (!strncasecmp (buf + index - offset, "cwd ", 4))
{
path_index = index + 4;
if (buf[path_index - offset] == '/')
{
strcpy (p->currdir, "/");
path_index++;
}
for (;;)
{
pi2 = path_index;
while (buf[pi2 - offset] != '\n' && buf[pi2 - offset] != '/')
pi2++;
if (buf[pi2-offset]=='\n' && buf[pi2-offset-1]=='\r')
remcaret=1;
else remcaret=0;
if (add_to_path (p->currdir, buf + path_index-offset, pi2 - path_index-remcaret))
{
// notify admin
syslog(nids_params.syslog_level,
"Ftpd exploit attempt, connection %s\n",adres(a_tcp->addr));
nids_killtcp (a_tcp);
return;
}
if (buf[pi2 - offset] == '\n')
break;
path_index = pi2 + 1;
}
}
index = index2 + 1;
}
p->last_newline = index - 1;
nids_discard (a_tcp, index - offset);
}
void
detect_ftpd (struct tcp_stream *a_tcp, struct supp **param)
{
if (a_tcp->nids_state == NIDS_JUST_EST)
{
if (a_tcp->addr.dest == 21)
{
struct supp *one_for_conn;
a_tcp->server.collect++;
one_for_conn = (struct supp *) malloc (sizeof (struct supp));
one_for_conn->currdir = malloc (1024);
strcpy (one_for_conn->currdir, "/");
one_for_conn->last_newline = 0;
*param=one_for_conn;
}
return;
}
if (a_tcp->nids_state != NIDS_DATA)
{
free ((*param)->currdir);
free (*param);
return;
}
do_detect_ftp (a_tcp, param);
}
int
main ()
{
if (!nids_init ())
{
fprintf(stderr,"%s\n",nids_errbuf);
exit(1);
}
nids_register_tcp (detect_imap);
nids_register_tcp (detect_ftpd);
nids_run ();
return 0;
}