Suricata IPS-NFQ模式
注:本文写于2019年,文中的相关概念的介绍摘抄出自哪里已经不记得,如有侵权请指出,本文将补充相关部分的引用。
1.Suricata运行模式
Suricata有两个运行模式的概念。
一个运行模式封装在RunModes(runmmode.c)结构体里,一个RunModes对应一组运行模式。Suricata支持的所有运行模式存储在runmodes数组中,定义为RunModes runmodes[RUNMODE_USER_MAX]。模式包括:“IDS+Pcap”模式组、“File+Pcap”模式组、“IPS+NFQ”模式组、“UnixSocket”模式组等(另外还有其他一些内部模式,如:“列出关键字”模式、“打印版本号”模式等,这些没有存储在runmodes数组中)。模式组的选择决定了Suricata的数据来源、实现功能(IDS或IPS)、和输出的方式。本小节就把这种运行模式的概念称作:运行模式组。
另一个运行模式的概念来源于配置文件里的runmode字段。Suricata由几个称为线程,线程模块和队列的“构建块”组成。Suricata是多线程的,因此多个线程同时处于活动状态。线程模块是功能的一部分。一个模块例如用于解码分组,另一个模块是检测模块,另一个模块是输出模块。数据包可以由多个线程处理。数据包将通过队列传递到下一个线程。数据包将由一个线程一次处理,但引擎一次可以处理多个数据包(Max-pending-packets)。一个线程可以有一个或多个线程模块。如果他们有更多的模块,他们只能一次活动。
这种线程,模块和队列排列在一起的方式就是配置文件里这个runmode的含义。Suricata从官方介绍文件看目前有三种:Single、Autofp、Workers。
附上官网关于这三种运行模式的介绍:https://suricata.readthedocs.io/en/suricata-4.1.4/performance/runmodes.html ,本小节称这种配置文件里的运行模式就叫运行模式,区分运行模式组。
官方文件表明,通常情况下,Workers运行模式表现最佳。在此模式下,NIC /驱动程序确保数据包在Suricata的处理线程上正确平衡,每个分组处理线程包含完整的分组管道。
运行模式和运行模式组的关系是:每一个模式组,可以包含若干个运行模式,运行模式的注册,则是为各个模式组(如RunModeIpsNFQRegister)添加其所支持的运行模式(通过调用RunModeRegisterNewRunMode),并定义该组的默认运行模式,以及非常重要的:注册各个模式下的初始化函数,等后续初始化阶段确定了具体的运行模式后,就会调用这里注册的对应的初始化函数,对该模式下的运行环境进行进一步配置。
IPS-NFQ模式组在Runmode-nfq.c 的RunModeIpsNFQRegister() 函数中注册了两种运行模式:autofp 和 workers 模式,其中autofp是默认的运行模式。
2. Suricata IPS-NFQ模式
2.1 IPS-NFQ模式与网络防火墙iptables/netfilter
Suricata IPS-NFQ模式是通过和linux下通用的网络防火墙iptables/netfilter的连通来达到阻断网络的目的。通过iptables 将网络报文发送到特定的队列中去,放入到用户态中,然后通过suricata进行规则匹配,最后返回对报文的处理。
一般在iptables中的target有以下六种:
值 | 宏定义 | 含义 |
---|---|---|
0 | NF_DROP | 丢弃数据包 |
1 | NF_ACCEPT | 数据包通过,继续迭代 |
2 | NF_STOLEN | 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权 |
3 | NF_QUEUE | 将数据包注入不同的队列(目标队列号位于判决的高16位) |
4 | NF_REPEAT | 再次迭代相同的循环(再次调用该钩子函数,即重新跳到该表中的第一条规则?) |
5 | NF_STOP | 接受,但不要继续迭代,不再进入链表中后续的hook节点,而NF_ACCEPT则还需要进入后续hook点检查。 |
注:
1.不得使用判决NF_STOLEN,因为它在内核中具有特殊含义。
2.使用NF_REPEAT时,防止同一数据包重新排队的一种方法是使用nfq_set_verdict2设置nfmark,并设置nefilter规则,以便仅在尚未设置标记时对数据包进行排队。
Suricata IPS-NFQ模式中用到的判决有NF_DROP、NF_ACCEPT、NF_QUEUE、NF_REPEAT这四种。
2.2 IPS-NFQ模式里的判决模式
Suricata IPS-NFQ模式里有三种判决模式可选:accept、repeat、route。在suricata.yaml 配置文件里设置。accept模式下默认的判决是NF_ACCEPT。repeat模式下默认的判决是NF_REPEAT。route模式下默认的判决是NF_QUEUE。注意以上三种模式的判决结果都只存在两种:默认判决或者NF_DROP。
当采用accept模式时,只会给出DROP或者ACCEPT判决,只需在iptables的规则里写入类似如下规则:
iptables -I FORWARD -j NFQUEUE –-queue-num X
当采用repeat模式时, Suricata需要用到判决NF_REPEAT时,Suricata用NF_REPEAT判决会再次迭代相同的循环(再次调用该钩子函数),所以使用下面的iptables规则可以使得被suricata标记过的数据包不再发送到suricata,避免不必要的循环。
iptables -I FORWARD -m mark ! --mark $MARK/$MASK -j NFQUEUE
配置文件:
# repeat-mark: 1
# repeat-mask: 1
当采用route模式时,Suricata需要用到判决NF_QUEUE。这是最好要保证指定跳转的队列,不要是由Suricata监听的队列,以免造成死循环。从Suricata的代码来看,Suricata的repeat模式对数据包进行了防止循环的检验,但是route模式并没有做防止死循环的检验。(经测试,将suricata的route模式的目的队列设置为suricata监听的队列时,数据包会一直循环发送到suricata)
当在suricata命令行输入如下命令让suricata读取特定编号的nfqueue时:
sudo suricata -c /etc/suricata/suricata.yaml -q 0
在suricata.c文件的ParseCommandLine函数里会对这个命令做解析,将这个queue的编号作为参数调用source-nfq.c中的NFQRegisterQueue()函数。这个函数会把这个链编号注册到一个静态全局数组NFQQueueVars g_nfq_q[NFQ_MAX_QUEUE]里面。
建议在source-nfq.c的NFQInitConfig函数(被suricata.c的PostConfLoadedSetup函数调用)里读配置文件的route-queue时,在存储queue相关变量的数组g_nfq_q[NFQ_MAX_QUEUE]里比对一下,待跳转的链编号route-queue是不是在surcata接收的链编号里面。从而避免route模式下产生死循环。
2.3 suricata.yaml配置文件里与nfq相关的配置变量
suricata.yaml配置文件里与nfq相关的变量均在“nfq:”下面,所有可设置的配置变量如下:
nfq:
# mode: accept
# repeat-mark: 1
# repeat-mask: 1
# bypass-mark: 1
# bypass-mask: 1
# route-queue: 2
# batchcount: 20
# fail-open: yes
mode就是nfq模式下的判决模式,有三种判决模式:accept、repeat、route。已在上一节对这三种判决模式进行了介绍。
repeat-mark和repeat-mask是重复标记和标记掩码,iptables/netfilter框架下提供的数据包,有一个标记变量mark,可以对数据包做标记。repeat-mark和repeat-mask用于repeat模式下,对监听队列中的数据包做repeat标记,该模式下也会通过数据包的repeat标记对suricata监听队列的包做一个防止死循环的验证。
bypass-mark和bypass-mask是旁路标记和旁路掩码,但是这个bypass功能并不是运用的iptables/netfilter框架提供的bypass功能,而是由suricata提供的bypass功能。该功能的意义在于当一个数据包判定允许通过时,则直接允许该数据包所在流通过。bypass功能不仅限于NFQ模式。但是不明白为什么suricata要把这个bypass不设置enable或disable的形式而设置成标记和标记掩码并传到iptables/netfilter的mark变量里。
route-queue是在route模式下使用的,route模式默认的判定是NF_QUEUE,判决NF_QUEUE将数据包注入不同的队列(目标队列号位于判决的高16位)。route-queue就是目标队列号。
batchcount 是用于批量判决的一个参数,仅workers模式支持批量判决。所以batchcout仅在workers运行模式下可用,批量判决是libnetfilter_queue库提供的一种一次性提交对多个数据包的判决的方法。batchcount用于设定批量判决最多判定的数据包数量。
fail-open 是iptables/netfilter框架提供的一个选项,启用fail-open选项时,当suricata处理速度不够,内核队列数据包已满时,待排队的数据包不丢弃,而是允许接受。
2.4 Suricata IPS-NFQ模式下的解析流程
以Workers运行模式为例,Suricata IPS-NFQ模式下的解析流程如下图所示:
注:图片摘自其他博客
Workers运行模式
数据包处理线程中会依次经过如上图所示的几个模块,各模块功能如下
• Receive:从NFQUEUE中接收数据包,并将封装在Packet结构中,然后放入下一个缓冲区。(主要代码位于source-nfq.c)
• Decode:对数据包进行解码,主要是对数据包头部信息进行分析并保存在Packet结构中。
• StreamTCP:对数据包进行TCP流重组。
• Detect:检测数据包是否包含入侵行为。
• Verdict:对检测后的数据包进行判定,并将判定结果告诉内核,方便内核对数据包进行接收、丢弃等处理。
• RespondReject:通过libnet对那些要执行Reject操作的数据包进行相应的回应。
3. NFQ-Receive模块
3.1模块介绍
NFQ-Receive是NFQ模式下的Receive模块负责与NFQueue建立连接,并获取内核NFQueue上的排队的数据包,并将判决、标记、修改后的包(后两项如果有的话)传回iptables/netfilter框架。
3.2重要的变量介绍
1> NFQQueueVars
该结构体定义了一些与NFQueue相关的变量。在source-nfq.c中定义了一个静态全局数组static NFQQueueVars g_nfq_q[NFQ_MAX_QUEUE];每一条链对应维护一个NFQQueueVars变量。
typedef struct NFQQueueVars_
{
struct nfq_handle *h; /*使用libnetfilter_queue库中的nfq_open()函数连接到数据包队列返回的handle变量*/
struct nfnl_handle *nh;
int fd;
uint8_t use_mutex; /*标识是否会用到线程互斥变量*/
/* 2 threads deal with the queue handle, so add a mutex */
struct nfq_q_handle *qh; /*libnetfilter_queue库中的nfq_create_queue函数创建一个新的队列句柄并返回指向新创建的队列的nfq_q_handle*/
SCMutex mutex_qh; /*nfq_handle的互斥变量?*/
/* this one should be not changing after init */
uint16_t queue_num; /*queue的数量*/
/* position into the NFQ queue var array */
uint16_t nfq_index;
#ifdef DBG_PERF
int dbg_maxreadsize;
#endif /* DBG_PERF */
/* counters 对已收到(处理)的数据包进行计数 */
uint32_t pkts;
uint64_t bytes;
uint32_t errs;
uint32_t accepted;
uint32_t dropped;
uint32_t replaced;
struct {
uint32_t packet_id; /* id of last processed packet */
uint32_t verdict;
uint32_t mark;
uint8_t mark_valid:1;
uint8_t len;
uint8_t maxlen;
} verdict_cache; //在workers模式下使用批量判决用到。
} NFQQueueVars;
2> NFQPacketVars
该结构体是为了Packet变量所定义,其定义了来源为NFQ的数据包的一些参数。
typedef struct NFQPacketVars_
{
int id; /* this nfq packets id */
uint16_t nfq_index; /* index in NFQ array */
uint8_t verdicted;
uint32_t mark; /*要返回给iptables/netfilter框架的数据包标记*/
uint32_t ifi; /*接收数据包的接口*/
uint32_t ifo; /*数据包传出的接口*/
uint16_t hw_protocol;
} NFQPacketVars;
3> NFQThreadVars
该结构体是为了NFQ模式下的数据包处理线程定义的,每一个链都有一个处理线程。在source-nfq.c中定义了一个静态全局数组static NFQThreadVars g_nfq_t[NFQ_MAX_QUEUE] ;每一条链的处理线程对应维护一个NFQThreadVars变量。
typedef struct NFQThreadVars_
{
uint16_t nfq_index; //在g_nfq_t数组里的位置
ThreadVars *tv; //Suricata线程通用的线程变量结构体,存储线程用到的一些变量
TmSlot *slot; //线程槽,也是suricata线程里通用的一个概念
char *data; /** Per function and thread data */
int datalen; /** Length of per function and thread data */
CaptureStats stats;
} NFQThreadVars;
4> NFQCnf
该结构体的变量存储着suricata.yaml配置文件里对应的nfq下的字段,即在2.3小节介绍的所有字段。
typedef struct NFQCnf_ {
NFQMode mode;
uint32_t mark; //repeat模式设置的,如果判定为nf_repeat,那么就给这个数据包做个标记,表明数据包回到netfilter框架后继续给下一条规则处理
uint32_t mask;
uint32_t bypass_mark; // 旁路标记,当数据包打上这个标记时, 如果没有软件正在监听队列,netfilter规则集将不会删除排队的数据包,而是忽略该条nfqueue规则,跳到下一条iptables规则
uint32_t bypass_mask;
uint32_t next_queue; //目标队列,route模式下使用
uint32_t flags; //fail-open选项是否启用
uint8_t batchcount; //批量判定的数据包个数
} NFQCnf;
5>Packet
在Suricata中,用来封装数据包的结构体为Packet(decode.h struct Packet_),核心字段如下:
字段 | 含义 |
---|---|
src/dst、sp/dp、proto | 五元组信息:源/目的地址,源/目的端口号,传输层协议 |
flow | 数据包所属的流指针(类型为Flow_ *)。 |
ip4h、ip6h | 网络层数据指针。 |
tcph、udph、sctph、icmpv4/6h | 传输层数据指针。 |
payload、payload_len | 应用层负载指针及长度。 |
uint32_t pktlen; uint8_t *ext_pkt | 数据包的长度,以及指向数据包数据的指针 |
next、prev | 前一个/后一个数据包指针,用于组成双向链表。 |
flags | 数据包的一些标识。如包是否被修改过、数据包的标记是否被修改过等。详见下表 |
Packet.flags二进制各个位上的掩码代表的含义:
宏定义 | 值 | 含义 |
---|---|---|
PKT_NOPACKET_INSPECTION | 1 | 指示不应检查数据包头或内容的标志 |
PKT_NOPAYLOAD_INSPECTION | (1<<2) | 注明不应检查数据包内容 |
PKT_ALLOC | (1<<3) | 此运行已分配数据包,需要释放 |
PKT_HAS_TAG | (1<<4) | 数据包与标记匹配 |
PKT_STREAM_ADD | (1<<5) | 已将数据包负载添加到重新组装的流中 |
PKT_STREAM_EST | (1<<6) | 数据包是已建立流的一部分 |
PKT_STREAM_EOF | (1<<7) | 流处于EOF状态 |
PKT_HAS_FLOW | (1<<8) | 有无流 |
PKT_PSEUDO_STREAM_END | (1<<9) | 结束流的伪包 |
PKT_STREAM_MODIFIED | (1<<10) | 数据包由流引擎修改,我们需要重新计算CSUM并重新注入/替换 |
PKT_MARK_MODIFIED | (1<<11) | 已修改包标记 |
PKT_STREAM_NOPCAPLOG | (1<<12) | 从PCAP日志中排除数据包,因为它是已达到重新组合深度的流的一部分。 |
PKT_TUNNEL | (1<<13) | 包在tunnel中 |
PKT_TUNNEL_VERDICTED | (1<<14) | 包在tunnel中并给出了判决 |
PKT_IGNORE_CHECKSUM | (1<<15) | 忽略校验和 |
PKT_ZERO_COPY | (1<<16) | 数据包来自零拷贝(ext_pkt不能释放) |
PKT_HOST_SRC_LOOKED_UP | (1<<17) | |
PKT_HOST_DST_LOOKED_UP | (1<<18) | |
PKT_IS_FRAGMENT | (1<<19) | 包是IP碎片?不是完整的数据 |
PKT_IS_INVALID | (1<<20) | 无效包 |
PKT_PROFILE | (1<<21) |
3.3重要的函数介绍
主要的函数在3.4节中基本都介绍到了,其余函数和更多细节的介绍可参看添加了注释的source-nfq.c源码。
3.4 代码逻辑
3.4.1数据包接收模块代码逻辑
1) 初始化
下图为数据包接收数据包初始化的代码逻辑。
图1 :
2) 数据包接收
图2和图3是数据包接收的逻辑。
图2:
图3:
3.4.2 数据包解码模块代码逻辑
1) 初始化
解码模块初始化的代码逻辑跟其他非NFQ模式基本一致,就是对解码线程变量进行初始化。
图4解码初始化函数:
2) 数据包解码
解码模块初始化的代码逻辑跟其他非NFQ模式基本一致,都是根据数据包的类型调用DecodeIPV4()或者DecodeIPV6()进行解码。
I.DecodeNFQ()函数是NFQ模式下数据包开始解析的地方,解码的一般流程如下:
DecodeNFQ() -> DecodeIPV4() ->DecodeTCP() -> II. tcp流重组
或者
DecodeNFQ() -> DecodeIPV4() ->DecodeUDP()-> AppLayerHandleUdp()-> AppLayerParserParse()
II. tcp流重组
StreamTcpReassembleAppLayer()(/src/stream-tcp-assemble.c)->
AppLayerHandleTCPData()( /src/app-layer.c )
将从tcp头中解析出来的应用层协议的相关信息传入 ->
AppLayerParserParse()函数,进行应用层解析。
图5解码函数:
3.4.3数据包判定模块代码逻辑
1) 初始化
数据包判定模块初始化对当前数据包所在NFQ的线程变量NFQThreadVars做了一些初始化。
图5判定初始化函数:
2) 数据包判定
图6是数据包判定的逻辑。
图6 数据包判定逻辑: