参考/翻译自官方的doc/API.html
简介
各种结构体与函数的声明定义在nids.h
,要使用libnids的程序必须包含该头文件并链接libnids.a
(或者libnids.so.xx
)。 main
函数通常是这样子:
1 | main() |
IP分片重组
首先定义函数
void ip_frag_func(struct ip * a_packet, int len)
在调用nids_init
之后,需要调用
nids_register_ip_frag(ip_frag_func);
或者nids_register_ip(ip_func);
注册之前定义的函数。前者用于处理libnids看到的所有IP包,后者处理没有分片或者已被重组的包。a_packet
指向一个接收到的数据包,len
是包的长度。
TCP流重组
定义回调函数
void tcp_callback(struct tcp_stream * ns, void ** param)
tcp_stream
结构提供了一个TCP连接的所有信息,包括两个half_stream
结构体字段(client
和server
)。
tcp_stream
有个nids_state
字段,tcp_callback
的行为依赖于它。
- ns->nids_state==NIDS_JUST_EST
ns
描述了一个刚刚建立的连接,tcp_callback
必须决定在之后该连接有新数据到达时是否被通知。如果需要考虑该连接,tcp_callback
将通知libnids期望接收的数据(包括data to client, to server, urgent data to client, urgent data to server),然后函数返回。 - ns->nids_state==NIDS_DATA 新数据到达,
half_stream
结构包含了数据所在的缓冲区。 nids_state
等于以下值时,- NIDS_CLOSE
- NIDS_RESET
- NIDS_TIMED_OUT
表明连接已关闭。
tcp_callback
应该释放分配的资源(如果有)。ns->nids_state==NIDS_EXITING
libnids正在退出。这是程序最后一次机会可以使用任何存储在half_stream
缓冲区中的数据。当从一个捕获文件而不是网络中读取流量,libnids可能看不到到close, reset, timeout这些状态。如果程序有未处理的数据(比如来自nids_discard()
的数据),这种情况下就允许程序处理这些数据。
一个简单的应用
下面是一个简单的程序,它把libnids看到的所有TCP连接中交换的数据输出到stderr
。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include "nids.h"
#define int_ntoa(x) inet_ntoa(*((struct in_addr *)&x))
// struct tuple4 contains addresses and port numbers of the TCP connections
// the following auxiliary function produces a string looking like
// 10.0.0.1,1024,10.0.0.2,23
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;
}
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)
{
// connection described by a_tcp is established
// here we decide, if we wish to follow this stream
// sample condition: if (a_tcp->addr.dest!=23) return;
// in this simple app we follow each stream, so..
a_tcp->client.collect++; // we want data received by a client
a_tcp->server.collect++; // and by a server, too
a_tcp->server.collect_urg++; // we want urgent data received by a
// server
#ifdef WE_WANT_URGENT_DATA_RECEIVED_BY_A_CLIENT
a_tcp->client.collect_urg++; // if we don't increase this value,
// we won't be notified of urgent data
// arrival
#endif
fprintf (stderr, "%s established\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_CLOSE)
{
// connection has been closed normally
fprintf (stderr, "%s closing\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_RESET)
{
// connection has been closed by RST
fprintf (stderr, "%s reset\n", buf);
return;
}
if (a_tcp->nids_state == NIDS_DATA)
{
// new data has arrived; gotta determine in what direction
// and if it's urgent or not
struct half_stream *hlf;
if (a_tcp->server.count_new_urg)
{
// new byte of urgent data has arrived
strcat(buf,"(urgent->)");
buf[strlen(buf)+1]=0;
buf[strlen(buf)]=a_tcp->server.urgdata;
write(1,buf,strlen(buf));
return;
}
// We don't have to check if urgent data to client has arrived,
// because we haven't increased a_tcp->client.collect_urg variable.
// So, we have some normal data to take care of.
if (a_tcp->client.count_new)
{
// new data for the client
hlf = &a_tcp->client; // from now on, we will deal with hlf var,
// which will point to client side of conn
strcat (buf, "(<-)"); // symbolic direction of data
}
else
{
hlf = &a_tcp->server; // analogical
strcat (buf, "(->)");
}
fprintf(stderr,"%s",buf); // we print the connection parameters
// (saddr, daddr, sport, dport) accompanied
// by data flow direction (-> or <-)
write(2,hlf->data,hlf->count_new); // we print the newly arrived data
}
return ;
}
int
main ()
{
// here we can alter libnids params, for instance:
// nids_params.n_hosts=256;
if (!nids_init ())
{
fprintf(stderr,"%s\n",nids_errbuf);
exit(1);
}
nids_register_tcp (tcp_callback);
nids_run ();
return 0;
}
libnids中基本的结构体与函数
1 | struct tuple4 // TCP connection parameters |
1 | struct half_stream // structure describing one side of a TCP connection |
1 | struct tcp_stream |
在上面的示例中,tcp_callback
输出hlf-data
的数据到stderr
,后续不再需要此数据。在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
返回后,libnids将从data
中丢弃起始的至多num_bytes
字节,同时会更新offset
字段,移动剩余的字节到缓冲区起始处。如果nids_discard
从未调用,则hlf->data
缓冲区恰好包含hlf->count_new
字节。通常情况下,hlf->data
中的字节数等于hlf->count
减去hlf->offset
。
由于nids_discard
函数,我们不需要将接收的字节复制到另外的缓冲区,hlf->data
总是包含尽可能多的字节。经常需要为每对(libnids_callback, tcp stream)
维护辅助的数据结构,如果希望检测针对wu-ftpd的攻击(涉及到在服务器上创建深层目录),需要将ftpd守护进程的当前目录存到某处,ftp客户端发送的“CWD”指定将改变当前目录,这是tcp_callback
第二个参数的用途,它是指向每个(libnids_callback, tcp stream)
对私有数据指针的指针,用法如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void
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_callback
的函数跟踪同一个TCP流,这会带来一个非默认的异常。
通过修改全部变量nids_params
来修改libnids的参数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65struct nids_prm
{
int n_tcp_streams; // 用于存储tcp_stream结构体的哈希表大小
// libnids同时跟踪不超过3 / 4 * n_tcp_streams个连接
// 默认值:1040,如果为0,不重组TCP流。
int n_hosts; // 用于存储IP分片重组信息的哈希表的大小,默认值:256
char * filename; // 从该文件中捕获数据包
// 文件必须是libpcap格式,device必须为NULL
// 默认值:NULL
char * device; // libnids将要监听数据包的接口
// 默认值为NULL,表明通过调用pcap_lookupdev决定device
// "all"使libnids尝试捕获所有接口的数据包
// "all"要求Linux内核>2.2.0,libpcap>=0.6.0
int sk_buff_size; // Linux内核定义的sk_buff结构体大小
// 如果该参数与sizeof(struct sk_buff)不同,libnids会被
// 绕过,通过攻击libnis的资源管理。
// 如果不放心,可以检查sizeof(sk_buff)并校正这个参数。
// 默认值:168
int dev_addon; // sk_buff结构为网络接口信息保留的字节数
// 如果dev_addon==-1,在nids_init()中会根据
// 监听的接口类型自动校正。
// 默认值:-1
void (*syslog)(); // see description below the nids_params definition
int syslog_level; // 如果nids_params.syslog==nids_syslog,
// 该字段决定syslogd使用的syslogd
// 默认值:LOG_ALERT
int scan_num_hosts;// 存储端口扫描信息的哈希表大小
// 如果为0,关闭端口扫描检测
// 默认值:256
int scan_num_ports;// 同一个源需要扫描的TCP端口数目
// 默认值:10
int scan_delay; // 两个端口间暂停不超过scan_delay毫秒
// 便于使libnids报告端口扫描尝试
// 默认值:3000
void (*no_mem)(); // 当libndis耗尽内存时调用,它应该终止当前进程。
// terminate the current process
int (*ip_filter)(struct ip*); // 当一个IP包到达时调用该函数
// 如果ip_filter返回非零值,继续处理数据包,否则丢弃
// 这样可以监控定向至指定主机的流量,而不是整个子网。
// 默认函数(nids_ip_filter)总返回1
char *pcap_filter; // 用于pcap(3)的字符串过滤,默认值为NULL。
// 这会应用到链路层,因此像"tcp dst port 23"的过滤器
// 不会正确地处理分片流量,应该加上
// "or (ip[6:2] & 0x1fff != 0)"处理所有分片的数据包
int promisc; // 如果非0,libnids读取数据包的device(s)为混杂模式
// 默认值:1
int one_loop_less; // 默认禁用
int pcap_timeout; // pcap_open_live的"timeout"参数,默认1024ms
// 如果需要更快的响应可以降低该值
// libnids-1.20开始出现
int multiproc; // 如果非零,那么IP分片重组和TCP流重组在不同线程进行
// 需要使用glib-2.0编译libndis ,使libnids使用两个线程
// 一个接收来自libpcap的IP分片
// 另外一个优先级较低的,用于处理分片,流并通知回调函数
// 最好使用nids_run(),这个行为对用户是不可见的。
// nids_next()使用这个功能没用,因为
// 必须为每个数据包启动并结束线程。
// 启用该选项,全局变量(nids_last_pcap_header
// 和nids_last_pcap_data)可能
// 不会正确地指向回调函数中处理的数据包
int queue_limit; // 限制排队的数据包的数目,当multiproc=true时使用
// 默认值:20000
int tcp_workarounds; // 为非RFC兼容的TCP/IP栈启用解决方法,不保证无害。
pcap_t *pcap_desc; // pcap描述符
} nids_params;
nids_params
的syslog
字段默认为函数nids_syslog
的地址:
void nids_syslog (int type, int errnum, struct ip *iph, void *data);
nids_params.syslog
函数用于报告异常条件,比如端口扫描企图,不可用的TCP首部标志或者其他。该字段应该是某个自定义事件记录函数的地址。定义在libnids.c
中的nids_syslog
函数是一个如何解码传给nids_params.syslog
参数的例子。nids_syslog
记录信息到系统守护进程syslogd
。
如果需要处理UDP数据包,应该定义
void udp_callback(struct tuple4 * addr, char * buf, int len, struct ip * iph);
并注册:nids_register_udp(udp_callback)
,addr
参数包含地址信息,buf
指向UDP包携带的数据,len
是数据长度,iph
指向包含该UP包的IP包。校验和已验证。
其他有用的技巧
void nids_killtcp(struct tcp_stream * a_tcp)
通过发送RST报文段终止a_tcp
描述的连接,并不保证可靠。
使用nids_run()
有一个缺点——应用完全为数据包驱动。有时候需要在没有数据包到达时执行某些任务,可以使用
int nids_next()
它调用pcap_next()
而不是pcap_loop
,它只处理一个数据包,如果没有可用数据包,进程会休眠。nids_next()
成功返回1,失败返回0,此时nids_errbuf
包含相应的错误消息。
通常,使用nids_next()
时,应用会在select()
中休眠,并测试包含fd
的read fd_set
的可读性,fd
通过调用
int nids_getfd()
获得,该函数成功时返回文件描述符,失败返回-1,nids_errbuf
存储错误信息。
类似地,
int nids_dispatch(int cnt)
包装了pcap_dispatch
,当想区分返回值(比如EOF和error)时,使用它代替nids_next()
更有利。
有几个原因需要跳过对某些数据包的校验和处理:
- 许多网卡驱动有能力计算外出数据包的校验和,这种情况下传给libpcap的数据包可以有未计算的校验和。
- 为了提升性能。
为通知libnids哪些数据包不需要被校验,应该分配一个nids_chksum_ctl
(定义在nids.h
中)数组
1 | struct nids_chksum_ctl |
并在
nids_register_chksum_ctl(struct nids_chksum_ctl *, int);
中注册,第二个参数是数组元素个数。
校验和函数首先依次检查该数组每个元素,如果当前数据包的源IPSRCIP
满足条件
(SRCIP&chksum_ctl_array[i].mask)==chksum_ctl_array[i].netaddr
那么如果action
字段是NIDS_DO_CHKSUM
,计算校验和,如果action
字段是NIDS_DONT_CHKSUM
,不计算校验和。如果数据包不匹配数组中任何一个元素,默认动作是执行校验和计算。
samples/chksum_ctl.c
中有一个例子。
头文件nids.h
定义了常数NIDS_MAJOR (1)
和NIDS_MINOR (21)
,可以运行时决定libnids的版本,nids.h
也曾定义过HAVE_NEW_PCAP
,但在1.19版本后已经废弃。
通常,TCP流携带的数据可以分为协议依赖的记录,TCP的回调函数能接收一定量数据,其中包含多个记录,因此,回调函数应该在接收的整个数据中迭代执行协议解析程序,这增加了代码的复杂性。
如果nids_params.one_loop_less
非零,libnids的行为略有变化。如果回调函数使用了一些(不是所有)新到达的数据,libnids立即再次调用它。缓冲区中只有未处理的数据,rcv->count_new
也会恰当地减少。这样,此时回调函数只需要处理一条记录——libnids将会再次调用它,直到不再剩下新数据或者没有数据可被处理。不幸地是这个行为在2+个回调函数读取同一半TCP连接时会导致比较大的语义问题,因此,如果nids_params.one_loop_less
非零,禁止将2个或更多的回调函数附加到同一半的TCP流上。又不幸地,现有的接口不能将这个错误传给回调函数。
上一次观察到的数据包的pcap头部导出如下:
extern struct pcap_pkthdr *nids_last_pcap_header;
可以使用它获取时间戳,得到更高的准确性并保存系统调用。
1.21的新特性
nids_last_pcap_data
是一个新的外部变量,用于获取最后一个PCAP帧的数据,就像用nids_last_pcap_header
获取最后一个PCAP帧的头部。
nids_linkoffset
是一个新的外部变量,用于获取当前PCAP设备在链路层和网络层计算的偏移,可以用从在ip_func
中获得的经过IP分片重组的数据包重构PCAP帧,通过从nids_last_pcap_data
起始处复制同样多的字节,像这样:
1 | void ip_callback(struct ip *pkt, int len) |
nids_params.pcap_desc
可能在libnids之外使用pcap_handler
,所需要做的是复制pcap_t
指针(通过pcap_open_live(), pcap_open_dead()
或pcap_open_offline()
返回)到nids_params.pcap_desc
并调用nids_pcap_handler()
。注意:libnids不知道当传递数据包给nids_pcap_handler()
时何时完成,所以必须调用nids_exit()
告诉libnids释放资源。
nids_params.tcp_workarounds
是一个新的运行时选项,为错误的TCP实现提供额外检查。如果该选项非零,libnids会将一个强行关闭的TCP连接设为NIDS_TIMED_OUT
状态。
nids_free_tcp_stream()
是一个新的外部函数,可以强迫libnids不再跟踪一个TCP流。需要注意的是,在注册的回调函数中为一个早已处于closing状态(NIDS_CLOSE, NIDS_TIMED_OUT, NIDS_RESET or NIDS_EXITING
)的TCP流调用nids_free_tcp_stream
会造成二次释放(libnids会在tcp_callback
返回时在内部调用nids_free_tcp_stream()
),程序会因此崩溃。
nids_unregister_ip_frag(), nids_unregister_ip(), nids_unregister_udp()
和nids_unregister_tcp()
是新的外部函数,可以解除之前用对应nids_register_*()
注册过的回调函数。
tcp_stream.user
是传给TCP回调函数的新字段,类似于void **param
,但它对相同TCP连接的所有回调函数都是可见的,而param
是每个回调函数特有的。
FAQ
对于一个连接X,只获取到server发来的数据
可能是在X的client端运行libnids,并且主机的网卡驱动在硬件上提供了校验和的计算,于是libnids看到client发的数据包的校验和没有计算就把它丢了。解决方法是用
nids_register_chksum_ctl()
,跳过校验和计算。如何使libnids追踪已建立的TCP连接
特意没有实现,TCP的某些关键信息只出现在SYN包中。如果确实需要这个功能,可以尝试包含libnids-track-established.patch。