Suricata的解码模块与数据包获取模块是一一对应的,例如DecodePcap对应ReceivePcap,DecodeAFP对应ReceiveAFP。
然而,我们知道数据包格式都是协议规定的,因此核心的数据包解码流程一定会是固定的。例如,对于常规的以太网包IPV4包TCP包,无非是DecodeEthernet->DecodeIPV4->DecodeTCP->…
那么,这里为什么需要设置不同的解码模块呢?主要原因是因为,各种数据源对协议的支持可能不同,并且也需要相应对Packet进行些不同设置。为了避免把这些差异处理代码集中放在一个诸如DecodePacket这样的通用函数中,因此Suricata干脆就做了划分,不同数据源对应不同解码模块。其实随着后面分析可以看到,每个解码模块函数都大同小异,在做完针对各自数据源的特殊处理后,都会调用DecodeEthernet、DecodePPP、DecodeSll等通用解码函数进行进一步处理。
例如,据我用肉眼做diff,DecodePcap和DecodeAFP的内容完全一样,而DecodePfring却相对更简单,只有对Ethernet的解码,因此可以猜测pfring数据源大概只支持以太网。另一方面,DecodePcapFile又完全不同了,其中有些针对Flow的特殊处理,并且解码也是通过回调函数来调用的,具体不知道是调的什么函数。
与前面数据源的类似,下面我只记录常用的实时pcap模式下的解码模块的实现,也就是DecodePcap模块。若遇到与前面可能重复的内容,这里可能就只简单提一下,避免冗余。
模块注册是跟ReceivePcap模块紧挨着的,与后者不同的是:
初始化函数为DecodePcapThreadInit。这个模块并没有初始化数据,因此initdata没有使用。函数内部会新建一个DecodeThreadVars结构体,用于保存该线程模块的上下文。该结构的重要字段如下:
字段 | 含义 |
udp_dp_ctx | 用于UDP包的应用层协议检测,通过AlpProtoFinalize2Thread初始化。 |
counter_* | 解码模块所注册的性能计数器ID,例如counter_ipv4计数器用于统计IPV4数据包个数。 |
函数中首先通过DecodeThreadVarsAlloc新建一个DecodeThreadVars,然后调用DecodeRegisterPerfCounters注册所需的计数器。
如上所述,模块的执行函数为DecodePcap。关于如何执行到这里,细节都在上一篇里记录了。DecodePcap的原型为:
TmEcode DecodePcap(ThreadVars *tv, Packet *p, void *data, PacketQueue *pq, PacketQueue *postpq)
其中,data为初始化时填充的DecodeThreadVars,pq为解码模块所嵌入的slot的slot_pre_pq,postpq则为slot_post_pq(可能为NULL)。
函数内部将根据Packet的datalink类型,分别调用不同的解码函数:
数据链路类型 | 解码函数 | 说明 |
LINKTYPE_LINUX_SLL | DecodeSll | libpcap使用的伪协议头,用于从"any"设备抓包或某些链路层头无法获取的情况,详见:Linux cooked-mode capture (SLL) 。 |
LINKTYPE_ETHERNET | DecodeEthernet | 以太网协议包。LINKTYPE_ETHERNET宏定义为pcap中的DLT_EN10MB(10Mb命名是历史原因,参考下面的列表)。 |
LINKTYPE_PPP | DecodePPP | PPP协议包。参见:RFC 1661 - The Point-to-Point Protocol (PPP)。 |
LINKTYPE_RAW | DecodeRaw | 原始IP数据包。即直接以IPv4或IPv6头开始。LINKTYPE_RAW宏定义为pcap中的DLT_RAW。 |
注:按照注释,Suricata中是想套用libpcap中的link type定义,完整的列表可参见:LINK-LAYER HEADER TYPES。
DecodeEthernet函数流程:
以太网承载类型 | 解码函数 | 说明 |
ETHERNET_TYPE_IP | DecodeIPV4 | IPv4数据包 |
ETHERNET_TYPE_IPV6 | DecodeIPV6 | IPv6数据包 |
ETHERNET_TYPE_PPPOE_SESS | DecodePPPOESession | PPPoE会话阶段数据包,见:PPPoE - 维基百科 |
ETHERNET_TYPE_PPPOE_DISC | DecodePPPOEDiscovery | PPPoE发现阶段数据包 |
ETHERNET_TYPE_VLAN | DecodeVLAN | VLAN数据包,见:虚拟局域网 - 维基百科 |
PPPoE和VLAN都是夹在Ethernet协议和IP协议之间的,而相比PPPoE来说,VLAN在公司环境中更加普遍,因此下面只记录VLAN解码。
由于VLAN可能嵌套,因此Packet结构体中使用以下相应字段进行记录:
字段 | 含义 |
vlan_idx | 当前的vlan层数,初始为0,最多为2,即最多只能嵌套一层VLAN,否则会报错。 |
vlan_id[2] | 记录每一层的VLAN ID(共12位,可表示4096个VLAN),通过GET_VLAN_ID获得。 |
vlanh[2] | 记录每一层的VLAN头指针(VLANHdr *类型)。 |
DecodeVLAN函数流程:
DecodeIPV4首先会调用完成实际解码的DecodeIPV4Packet函数,该函数流程为:
在Packet结构中,用于表示IPv4选项的结构体为IPV4Vars,其重要字段包括:
字段 | 含义 |
ip_opts | IP选项数组,每一项存储一个IPV4Opt类型的选项,数组长度为IPV4_OPTMAX(40)。 |
ip_opt_cnt | IP选项数目,也就是ip_opts数组的实际长度。 |
o_rr、o_qs、o_ts… | IP选项指针(IPV4Opt *),用于对选项数组的直接访问以及复制跟踪(注释里这么写的)。 |
IP选项的一般格式是:1个字节的代码(code),一个字节的长度(len),一个字节的指针(ptr),指针的值从1开始计数,指向IP选项的内容,一般其值为4(跳过前面3字节的code, len, ptr),长度包括前面3字节在内的整个IP选项,最大值为40(引自:IP选项概述)。相应地,表示单个选项的IPV4Opt结构体定义为:
typedef struct IPV4Opt_ {
/** \todo We may want to break type up into its 3 fields
* as the reassembler may want to know which options
* must be copied to each fragment.
*/
uint8_t type; /**< option type */
uint8_t len; /**< option length (type+len+data) */
uint8_t *data; /**< option data */
} IPV4Opt;
其中,几个重要的type包括:
类型 | 含义 |
IPV4_OPT_EOL | End of List:表示IP首部的选项到此结束。 |
IPV4_OPT_NOP | No Operation:用作占位符。 |
IPV4_OPT_RR | Record Route:路由记录选项,每一个路由器都把自己的出口IP地址记录到该选项的IP清单中。 |
IPV4_OPT_LSRR | Loose Source Route:松散源路由选项,ptr指向一组IP地址,数据包必须按顺序经过其中的每一个,允许其中经过其它路由器。 |
IPV4_OPT_SSRR | Strict Source Route:严格源路由选项,与上面类似,但不允许经过其它路由器。 |
IPV4_OPT_TS | Timestamp:IP时间戳选项,用途见:关于IP选项 - 网络分析 - CSNA网络分析论坛。 |
DecodeIPV4Options函数流程为:
DecodeIPV4Packet返回后,DecodeIPV4继续执行:
类型 | 处理方式 |
IPPROTO_TCP | DecodeTCP |
IPPROTO_UDP | DecodeUDP |
IPPROTO_ICMP | DecodeICMPV4 |
IPPROTO_GRE | DecodeGRE |
IPPROTO_SCTP | DecodeSCTP |
IPPROTO_IPV6 | IPv6-in-IPv4 tunnel:PacketPseudoPktSetup->DecodeTunnel->放入pre队列 |
IPPROTO_IP | 若为PPP VJ uncompressed包则直接调用DecodeTCP(这种情况不太明白) |
DecodeIPV6首先会调用完成实际解码的DecodeIPV6Packet函数,该函数流程为:
然后,根据next header(见IPv6头格式)字段(可以为下一层协议头类型,也可以为扩展头类型,参考IPV6扩展报文头),进行不同处理:
类型 | 处理方式 |
IPPROTO_TCP/UDP/SCTP | 与IPv4解码函数中的处理相同 |
IPPROTO_ICMPV6 | DecodeICMPV6 |
IPPROTO_IPIP/IPV6 | DecodeIPv4inIPv6/DecodeIP6inIP6,参考:RFC 2473:Generic Packet Tunneling in IPv6 |
IPPROTO_FRAGMENT | 分段扩展头:当发送方必须对超出MTU的数据进行分段时使用该扩展报头。调用DecodeIPV6ExtHdrs进行处理,下同。 |
IPPROTO_HOPOPTS |
逐跳扩展头:用来携带一些可选信息,数据包所经由的所有路由器都必须处理该信息。 |
IPPROTO_ROUTING | 路由扩展头:用来定义源路由。 |
IPPROTO_NONE | NO NExt,即后面没有任何header了。 |
IPPROTO_DSTOPTS | 目的地扩展头:用来携带那些仅需要数据包目的地节点处理的可选信息。 |
IPPROTO_AH | 认证头扩展头:封装IPSec数据。 |
IPPROTO_ESP | Encapsulating Security Payload(ESP) 扩展头:也用于封装IPSec数据。 |
IPPROTO_ICMP | 发现一个承载ICMPv4的IPv6数据包,添加IPV6_WITH_ICMPV4事件。 |
Suricata中用于表示IPv6扩展头的结构体为IPV6ExtHdrs,其重要字段如下:
字段 |
含义 |
ip6_exthdrs[IPV6_MAX_OPT] | IP选项头数组,类型为IPV6GenOptHdr,IPV6_MAX_OPT定义为40。 |
ip6_exthdrs_cnt | 选项头个数,及ip6_exthdrs的实际长度。 |
ip6fh、ip6rh、ip6ah… | 特定选项指针。 |
ip6hh_opt_hao、ip6hh_opt_ra、… | 用途未知 |
而IPV6GenOptHdr定义为:
typedef struct IPV6GenOptHdr_ {
uint8_t type;
uint8_t next;
uint8_t len;
uint8_t *data;
} IPV6GenOptHdr;
处理IP扩展头的DecodeIPV6ExtHdrs函数流程如下:
无论是直接调用下层解码,还是先处理IP扩展头再调用下层解码,完成后都会返回到DecodeIPV6中,继续后面的处理流程:
DecodeTCP首先调用DecodeTCPPacket完成实际的解码,其流程为:
Suricata中用于表示TCP选项的数据结构为TCPVars:
字段 | 含义 |
tcp_opts[TCP_OPTMAX] | TCP选项数组,类型为TCPOpt,TCP_OPTMAX定义为20。 |
tcp_opt_cnt | TCP选项个数 |
ts、sack、sackok、ws、mss | 特定选项指针 |
选项由TCPOpt结构体表示:(可参考TCP头部选项 )
typedef struct TCPOpt_ {
uint8_t type; /* 选项类型 */
uint8_t len; /* 选项长度 */
uint8_t *data; /* 内容指针 */
} TCPOpt;
其中type有以下几种(参考TCP头部选项功能详解):
类型 | 含义 |
TCP_OPT_EOL | 选项表结束。 |
TCP_OPT_NOP | 空操作,一般用于将TCP选项的总长度填充为4字节的整数倍。 |
TCP_OPT_MSS | 最大报文段长度选项,一般设置为MTU-40,关于MTU可参考:以太网最大帧和最小帧、MTU。 |
TCP_OPT_WS | 窗口扩大选项,用于增加TCP窗口值(16位,最大64KB),提高吞吐量。 |
TCP_OPT_SACKOK | 选择性确认(Selective Acknowledgements )选项,用在连接初始化时,表示是否支持SACK技术。 |
TCP_OPT_SACK | SACK实际工作的选项,可让发送端只重新发送丢失的TCP报文段。 |
TCP_OPT_TS | 时间戳选项,用于计算RTT,从而为TCP流量控制提供重要信息。 |
DecodeTCPOptions流程为:
DecodeTCPPacket返回后,DecodeTCP继续执行,剩下的唯一工作就是调用FlowHandlePacket,更新流表。
DecodeUDP首先调用DecodeUDPPacket完成实际解码,流程为:
DecodeUDP继续执行,调用DecodeTeredo尝试进行Teredo隧道解码,见RFC 4380:Teredo: Tunneling IPv6 over UDP through Network Address Translations (NATs)。
若解码成功,即这是一个Teredo隧道包,则直接调用FlowHandlePacket,并返回,不再进行应用层处理(因为已经知道这个包的用途了?)。
否则,在调用FlowHandlePacket后,再调用AppLayerHandleUdp对该UDP包的应用层数据进行进一步处理。
为什么解码TCP时没有调类似的AppLayerHandle函数呢? 其实有调用,但却是在StreamTCP模块,即对于TCP包的应用层分析是基于重组后的包。