Libnids-1.24 API

参考/翻译自官方的doc/API.html

简介

各种结构体与函数的声明定义在nids.h,要使用libnids的程序必须包含该头文件并链接libnids.a(或者libnids.so.xx)。 main函数通常是这样子:

1
2
3
4
5
6
7
8
9
main()
{
application private processing, not related to libnids
optional modification of libnids parameters
if (!nids_init() ) something's wrong, terminate;
registration of callback functions
nids_run();
// not reached in normal situation
}

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结构体字段(clientserver)。

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
2
3
4
5
struct tuple4 // TCP connection parameters
{
unsigned short source,dest; // client and server port numbers
unsigned long saddr,daddr; // client and server IP addresses
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct half_stream // structure describing one side of a TCP connection
{
char state; // socket state (ie TCP_ESTABLISHED )
char collect; // 如果>0,数据将被存在"data"缓冲区中,
// 否则这个方向的数据流会被忽略。
// samples/sniff.c中有一个使用该字段的例子
char collect_urg; // analogically, determines if to collect urgent
// data
char * data; // 正常数据的缓冲区
unsigned char urgdata; // 用于紧急数据的一字节缓冲区
int count; // 连接建立以来,添加到"data"缓冲区的字节数
int offset; // "data"缓冲区第一个字节在数据流中的偏移
int count_new; // 最近一次(这一次)添加到"data"缓冲区中的字节数
// 如果为0,没有新数据到达
char count_new_urg; // 如果不为0,新的紧急数据到达

... // other fields are auxiliary for libnids

};
1
2
3
4
5
6
7
8
struct tcp_stream
{
struct tuple4 addr; // connections params (saddr, daddr, sport, dport)
char nids_state; // logical state of the connection
struct half_stream client,server; // structures describing client and
// server side of the connection
... // other fields are auxiliary for libnids
};

在上面的示例中,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
23
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_tcpnids_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
65
struct 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_paramssyslog字段默认为函数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()中休眠,并测试包含fdread fd_set的可读性,fd通过调用

int nids_getfd()

获得,该函数成功时返回文件描述符,失败返回-1,nids_errbuf存储错误信息。

类似地,

int nids_dispatch(int cnt)

包装了pcap_dispatch,当想区分返回值(比如EOF和error)时,使用它代替nids_next()更有利。


有几个原因需要跳过对某些数据包的校验和处理:

  1. 许多网卡驱动有能力计算外出数据包的校验和,这种情况下传给libpcap的数据包可以有未计算的校验和。
  2. 为了提升性能。

为通知libnids哪些数据包不需要被校验,应该分配一个nids_chksum_ctl(定义在nids.h中)数组

1
2
3
4
5
6
struct nids_chksum_ctl
{ u_int netaddr;
u_int mask;
u_int action;
/* reserved fields */
};

并在

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
2
3
4
5
6
7
8
9
10
11
12
13
void ip_callback(struct ip *pkt, int len)
{
u_char *frame;
struct pcap_pkthdr ph;

frame = malloc(len + nids_linkoffset);
memcpy(frame, nids_last_pcap_data, nids_linkoffset);
memcpy(frame + nids_linkoffset, pkt, len);
ph.ts = nids_last_pcap_header->ts;
ph.caplen = ph.len = len + nids_linkoffset;
pcap_dump(nids_params.pcap_desc, &ph, frame);
free(frame);
}

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。

------ 本文结束 ------

版权声明

Memory is licensed under a Creative Commons BY-NC-SA 4.0 International License.
博客采用知识共享署署名(BY)-非商业性(NC)-相同方式共享(SA)
本文首发于Memory,转载请保留出处。