2019独角兽企业重金招聘Python工程师标准>>>
简介
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模块紧挨着的,与后者不同的是:
- 模块flags为TM_FLAG_DECODE_TM,表示这是一个解码模块。
- 这次模块执行函数不再是PktAcqLoop了,而是Func,设置为DecodePcap。
模块初始化
初始化函数为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。
Ethernet解码
DecodeEthernet函数流程:
- 若包长小于ETHERNET_HEADER_LEN(14),则调用ENGINE_SET_EVENT向本数据包添加一个ETHERNET_PKT_TOO_SMALL事件,并返回。
- 设置ethh指针(EthernetHdr *类型),并根据eth_type调用下一层的解码函数:
以太网承载类型 | 解码函数 | 说明 |
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解码
由于VLAN可能嵌套,因此Packet结构体中使用以下相应字段进行记录:
字段 | 含义 |
vlan_idx | 当前的vlan层数,初始为0,最多为2,即最多只能嵌套一层VLAN,否则会报错。 |
vlan_id[2] | 记录每一层的VLAN ID(共12位,可表示4096个VLAN),通过GET_VLAN_ID获得。 |
vlanh[2] | 记录每一层的VLAN头指针(VLANHdr *类型)。 |
DecodeVLAN函数流程:
- 若len小于VLAN_HEADER_LEN(4),则添加VLAN_HEADER_TOO_SMALL事件。
- vlan_idx是否大于或等于2,否则添加VLAN_HEADER_TOO_MANY_LAYERS事件。
- 设置vlanh[vlan_idx]和vlan_id[vlan_idx],并调用GET_VLAN_PROTO获取下层协议,最后递增vlan_idx。
- 与DecodeEthernet类似,根据proto调用下一层的解码函数。
IPv4解码
DecodeIPV4首先会调用完成实际解码的DecodeIPV4Packet函数,该函数流程为:
- 若len小于IPV4_HEADER_LEN(20),则添加IPV4_PKT_TOO_SMALL事件。
- 使用IP_GET_RAW_VER获取版本号,若不为4,则添加IPV4_WRONG_IP_VER事件。
- 设置ip4h指针(IPV4Hdr *类型)。
- 使用IPV4_GET_HLEN获取IP头长度,若不等于IPV4_HEADER_LEN,则也添加IPV4_HEADER_LEN事件。
- 使用IPV4_GET_IPLEN获取IP数据包长度(IP头+IP负载),若小于IP头长度,则添加IPV4_IPLEN_SMALLER_THAN_HLEN事件。
- 若len小于IP数据包长度,则添加IPV4_TRUNC_PKT事件,表示这是个被截断的包。
- 设置Packet的src和dst,通过SET_IPV4_SRC_ADDR和SET_IPV4_DST_ADDR实现。
- 若IPv4选项长度(hlen-20)不为0,则调用DecodeIPV4Options对其进行解码。
在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函数流程为:
- 检查选项长度是否为8的倍数,若不是则添加IPV4_OPT_PAD_REQUIRED事件,但不会返回,还会继续解码下去。
- 在剩余长度不为0的情况下,按下面步骤循环处理每一个选项。
- 首先,看是否为单字节的选项:
- 若当前字节为IPV4_OPT_EOL,则退出循环。
- 若当前字节为IPV4_OPT_NOP,则递增指针,减少剩余长度,继续。
- 接着,看是否为多字节的选项:
- 若剩余长度小于2,则添加IPV4_OPT_EOL_REQUIRED事件,退出。
- 若当前选项长度(当前选项的第2字节,即len)大于剩余长度,则添加IPV4_OPT_INVALID_LEN事件,退出。
- 填充ip_opts[ip_opt_cnt]的各个字段,其中data在剩余长度大于2的情况下指向第3个字节,否则为NULL。
- 根据type进行不同的处理,通用的处理流程为:
- 检查该类型选项是否已经被设置了,若是则添加IPV4_OPT_DUPLICATE事件,然后退出。
- 调用专用的(如IPV4OptValidateTimestamp)或通用的(IPV4OptValidateGeneric)验证函数,对选项合法性进行验证。
- 设置对应的选项指针,如对于type为IPV4_OPT_TS的选项,将o_ts指向前面填充的选项数组中那个项。
- 更新选项指针、剩余长度、ip_opt_cnt,继续循环。
DecodeIPV4Packet返回后,DecodeIPV4继续执行:
- 调用IPV4_GET_IPPROTO获取下层协议,存储在Packet的proto中。
- 分片情况处理:若offset大于0或者MF标志为1,表示这是一个分片包,将会交给重组模块的Defrag函数去处理。若返回不为NULL,表示已经重组完成了,那么就递归调用DecodeIPV4重新进行IP层的解码,然后把这个新生成的重组包扔到pre队列中去。接着,给当前数据包打上PKT_IS_FRAGMENT标志,然后返回。
- 根据proto进行不同处理:
类型 | 处理方式 |
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(这种情况不太明白) |
IPv6解码
DecodeIPV6首先会调用完成实际解码的DecodeIPV6Packet函数,该函数流程为:
- 若len小于IPV6_HEADER_LEN(40),直接返回(并没有添加事件)。
- 若IP版本不为6,添加IPV6_WRONG_IP_VER事件并返回。
- 设置ip6h指针(IPV6Hdr *类型)。
- 若len小于IPv6头长度+Payload Length(头部第5、6字节),则添加IPV6_TRUNC_PKT事件,表示这是一个被截断的IPv6包。
- 设置Packet的src和dst。
然后,根据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函数流程如下:
- 进入while(1)循环,循环内部根据当前的next header类型进行switch,按照扩展头或下一层协议头进行处理。
- 对于TCP、UDP等下一层协议头,直接掉下层相应的解码函数,然后返回即可。
- 对于IPv6扩展头,则分类型处理,通用的处理流程为:
- 获取扩展头长度hdrextlen。
- 填充ip6_exthdrs[ip6_exthdrs_cnt],然后ip6_exthdrs_cnt++.
- 设置特定类型的扩展头指针,例如IPV6_EXTHDR_SET_RH用于设置ip6rh。
- 更新next header类型、当前数据指针、数据包剩余长度(初始值为Payload Length,该字段包含了扩展头的长度)。
无论是直接调用下层解码,还是先处理IP扩展头再调用下层解码,完成后都会返回到DecodeIPV6中,继续后面的处理流程:
- 更新Packet的proto。
- 分片处理:若有fh扩展头,则调用Defrag进行重组,与IPv4重组的处理类似。
- 处理结束,返回。
TCP解码
DecodeTCP首先调用DecodeTCPPacket完成实际的解码,其流程为:
- 若len小于TCP_HEADER_LEN(20),添加TCP_PKT_TOO_SMALL事件并返回。
- 设置tcph指针(TCPHdr *类型)。
- 获取TCP头部长度hlen,若len小于hlen,则添加TCP_HLEN_TOO_SMALL事件并返回。
- 计算TCP选项长度(hlen – 20)tcp_opt_len,若大于TCP_OPTLENMAX(40),则添加TCP_INVALID_OPTLEN事件并返回。
- 若tcp_opt_len大于0,则调用DecodeTCPOptions对选项进行解码。
- 设置Packet的sp、dp、proto、payload、payload_len。
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流程为:
- 与处理IPv4选项类似,循环处理每一个选项。
- 先看是否为单字节选项:对于EOL,直接退出;对于NOP,前进。
- 再看是否为多字节选项:填充tcp_opts[tcp_opt_cnt],并设置特定选项指针。
DecodeTCPPacket返回后,DecodeTCP继续执行,剩下的唯一工作就是调用FlowHandlePacket,更新流表。
UDP解码
DecodeUDP首先调用DecodeUDPPacket完成实际解码,流程为:
- 若len小于UDP_HEADER_LEN(8),则添加UDP_HLEN_TOO_SMALL事件并返回。
- 设置udph指针(UDPHdr *).
- 若len小于UDP头部Length字段(UDP头+负载),则添加UDP_PKT_TOO_SMALL事件并返回。
- 若len不等于UDP头部Length字段,则添加UDP_HLEN_INVALID事件并返回。
- 设置Packet的sp、dp、proto、payload、payload_len。
DecodeUDP继续执行,调用DecodeTeredo尝试进行Teredo隧道解码,见RFC 4380:Teredo: Tunneling IPv6 over UDP through Network Address Translations (NATs)。
若解码成功,即这是一个Teredo隧道包,则直接调用FlowHandlePacket,并返回,不再进行应用层处理(因为已经知道这个包的用途了?)。
否则,在调用FlowHandlePacket后,再调用AppLayerHandleUdp对该UDP包的应用层数据进行进一步处理。
为什么解码TCP时没有调类似的AppLayerHandle函数呢? 其实有调用,但却是在StreamTCP模块,即对于TCP包的应用层分析是基于重组后的包。