Suricata源码阅读笔记:数据包解码

简介

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模块紧挨着的,与后者不同的是:

  1. 模块flags为TM_FLAG_DECODE_TM,表示这是一个解码模块。
  2. 这次模块执行函数不再是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函数流程:

  1. 若包长小于ETHERNET_HEADER_LEN(14),则调用ENGINE_SET_EVENT向本数据包添加一个ETHERNET_PKT_TOO_SMALL事件,并返回。
  2. 设置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函数流程:

  1. 若len小于VLAN_HEADER_LEN(4),则添加VLAN_HEADER_TOO_SMALL事件。
  2. vlan_idx是否大于或等于2,否则添加VLAN_HEADER_TOO_MANY_LAYERS事件。
  3. 设置vlanh[vlan_idx]和vlan_id[vlan_idx],并调用GET_VLAN_PROTO获取下层协议,最后递增vlan_idx。
  4. 与DecodeEthernet类似,根据proto调用下一层的解码函数。

IPv4解码

DecodeIPV4首先会调用完成实际解码的DecodeIPV4Packet函数,该函数流程为:

  1. 若len小于IPV4_HEADER_LEN(20),则添加IPV4_PKT_TOO_SMALL事件。
  2. 使用IP_GET_RAW_VER获取版本号,若不为4,则添加IPV4_WRONG_IP_VER事件。
  3. 设置ip4h指针(IPV4Hdr *类型)。
  4. 使用IPV4_GET_HLEN获取IP头长度,若不等于IPV4_HEADER_LEN,则也添加IPV4_HEADER_LEN事件。
  5. 使用IPV4_GET_IPLEN获取IP数据包长度(IP头+IP负载),若小于IP头长度,则添加IPV4_IPLEN_SMALLER_THAN_HLEN事件。
  6. 若len小于IP数据包长度,则添加IPV4_TRUNC_PKT事件,表示这是个被截断的包。
  7. 设置Packet的src和dst,通过SET_IPV4_SRC_ADDR和SET_IPV4_DST_ADDR实现。
  8. 若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函数流程为:

  1. 检查选项长度是否为8的倍数,若不是则添加IPV4_OPT_PAD_REQUIRED事件,但不会返回,还会继续解码下去。
  2. 在剩余长度不为0的情况下,按下面步骤循环处理每一个选项。
  3. 首先,看是否为单字节的选项:
    • 若当前字节为IPV4_OPT_EOL,则退出循环。
    • 若当前字节为IPV4_OPT_NOP,则递增指针,减少剩余长度,继续。
  4. 接着,看是否为多字节的选项:
    • 若剩余长度小于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继续执行:

  1. 调用IPV4_GET_IPPROTO获取下层协议,存储在Packet的proto中。
  2. 分片情况处理:若offset大于0或者MF标志为1,表示这是一个分片包,将会交给重组模块的Defrag函数去处理。若返回不为NULL,表示已经重组完成了,那么就递归调用DecodeIPV4重新进行IP层的解码,然后把这个新生成的重组包扔到pre队列中去。接着,给当前数据包打上PKT_IS_FRAGMENT标志,然后返回。
  3. 根据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函数,该函数流程为:

  1. 若len小于IPV6_HEADER_LEN(40),直接返回(并没有添加事件)。
  2. 若IP版本不为6,添加IPV6_WRONG_IP_VER事件并返回。
  3. 设置ip6h指针(IPV6Hdr *类型)。
  4. 若len小于IPv6头长度+Payload Length(头部第5、6字节),则添加IPV6_TRUNC_PKT事件,表示这是一个被截断的IPv6包。
  5. 设置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函数流程如下:

  1. 进入while(1)循环,循环内部根据当前的next header类型进行switch,按照扩展头或下一层协议头进行处理。
  2. 对于TCP、UDP等下一层协议头,直接掉下层相应的解码函数,然后返回即可。
  3. 对于IPv6扩展头,则分类型处理,通用的处理流程为:
    • 获取扩展头长度hdrextlen。
    • 填充ip6_exthdrs[ip6_exthdrs_cnt],然后ip6_exthdrs_cnt++.
    • 设置特定类型的扩展头指针,例如IPV6_EXTHDR_SET_RH用于设置ip6rh。
    • 更新next header类型、当前数据指针、数据包剩余长度(初始值为Payload Length,该字段包含了扩展头的长度)。

无论是直接调用下层解码,还是先处理IP扩展头再调用下层解码,完成后都会返回到DecodeIPV6中,继续后面的处理流程:

  1. 更新Packet的proto。
  2. 分片处理:若有fh扩展头,则调用Defrag进行重组,与IPv4重组的处理类似。
  3. 处理结束,返回。

TCP解码

DecodeTCP首先调用DecodeTCPPacket完成实际的解码,其流程为:

  1. 若len小于TCP_HEADER_LEN(20),添加TCP_PKT_TOO_SMALL事件并返回。
  2. 设置tcph指针(TCPHdr *类型)。
  3. 获取TCP头部长度hlen,若len小于hlen,则添加TCP_HLEN_TOO_SMALL事件并返回。
  4. 计算TCP选项长度(hlen – 20)tcp_opt_len,若大于TCP_OPTLENMAX(40),则添加TCP_INVALID_OPTLEN事件并返回。
  5. 若tcp_opt_len大于0,则调用DecodeTCPOptions对选项进行解码。
  6. 设置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流程为:

  1. 与处理IPv4选项类似,循环处理每一个选项。
  2. 先看是否为单字节选项:对于EOL,直接退出;对于NOP,前进。
  3. 再看是否为多字节选项:填充tcp_opts[tcp_opt_cnt],并设置特定选项指针。

DecodeTCPPacket返回后,DecodeTCP继续执行,剩下的唯一工作就是调用FlowHandlePacket,更新流表。

UDP解码

DecodeUDP首先调用DecodeUDPPacket完成实际解码,流程为:

  1. 若len小于UDP_HEADER_LEN(8),则添加UDP_HLEN_TOO_SMALL事件并返回。
  2. 设置udph指针(UDPHdr *).
  3. 若len小于UDP头部Length字段(UDP头+负载),则添加UDP_PKT_TOO_SMALL事件并返回。
  4. 若len不等于UDP头部Length字段,则添加UDP_HLEN_INVALID事件并返回。
  5. 设置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包的应用层分析是基于重组后的包。

转载于:https://my.oschina.net/openadrian/blog/186334

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值