NIDS开发之IP分片重组与TCP段重组技术详解

网络安全 潘老师 1小时前 3 ℃ (0) 扫码查看

NIDS(Network Intrusion Detection System,网络入侵检测系统)在网络安全领域发挥着重要的作用,NIDS的开发主要涵盖两个关键部分:DPI深度包解析和规则引擎。其中,DPI深度包解析是NIDS运作的基础,它通过监听网络流量,捕捉数据包并进行协议识别、字段拆解。常见的数据包捕捉工具包括libpcap、PF_RING、DPDK等,而像PacketBeat、NDPI、Zeek、Suricata则是常用的DPI深度包解析工具。

在TCP/IP协议栈中,数据包的重组涉及IP分片重组和TCP段重组两个重要方面。这两种重组操作对于NIDS来说意义重大,接下来我们详细探讨一下。

一、IP分片重组和TCP段重组基础概念

(一)IP分片重组

当一个IP数据报过大,无法在当前物理网络上直接传输时,就会被分割成多个较小的片段,也就是分片。每个分片都会作为独立的IP数据报在网络中传输,最终在目的地重新组装成原始的数据报,这个过程就是IP分片重组。

IP头部的标识字段、标志字段和分片偏移字段在重组过程中发挥关键作用。标识字段用于区分不同的数据报,标志字段中的MF(More Fragments)位表示后面是否还有更多分片,分片偏移字段则指明该分片在原始数据报中的位置。

(二)TCP段重组

TCP协议是面向连接且提供可靠数据传输服务的协议。在数据传输时,发送方可能会将数据分割成多个TCP段进行发送。由于网络路由和延迟等因素,这些TCP段到达接收方的顺序可能与发送顺序不一致。TCP利用序列号和确认机制,确保所有发送的数据都能按正确顺序重组,接收方会根据序列号对收到的TCP段进行排序,并等待丢失段的重传。

(三)NIDS涉及重组的原因

大家可能会疑惑,IP分片重组和TCP段重组不是TCP/IP协议栈的工作吗,为什么NIDS也需要处理呢?这是因为NIDS使用如libpcap或DPDK等工具抓取的是一帧数据,一个IP分片通常被封装在一帧中传输。例如,在http post一个较大的json数据时,数据可能会经过TCP段重组(如果没有IP分片的情况下),NIDS才能看到完整的json内容。在TCP/IP协议栈中,每一层对上层是透明的,但NIDS需要自己重组每一层的数据,以便进行深入分析。

(四)IP分片和TCP分段的区别

IP分片和TCP分段虽然都涉及数据的切分,但它们发生在网络协议栈的不同层次。IP分片发生在网络层,由IP协议处理,主要是因为网络链路层的MTU(最大传输单元)限制,当IP数据报大小超过MTU时就需要分片。而TCP分段发生在传输层,由TCP协议处理,它是为了适应MSS(最大分段大小),避免IP层分片,TCP会根据MSS对数据进行分段。TCP分段在基于TCP的应用层协议(如HTTP、FTP)中非常常见,而IP分片通常是网络层处理特殊情况的手段,理想情况下应尽量避免。

二、IP分片重组与TCP段重组示例

(一)IP分片示例

假设主机A要通过以太网向主机B发送一个4000字节的IP数据报,而网络路径的MTU为1500字节,IP首部长度为20字节。由于数据报总长度超过MTU,所以需要进行IP分片。

  1. 计算IP分片数量:每个分片的最大有效载荷为1500 – 20(IP首部) = 1480字节。总数据量为4000字节,因此需要分成4000 ÷ 1480 ≈ 3片。
  2. IP分片详细信息
    分片编号 数据范围 数据长度 偏移量(字节) 偏移量(单位) MF标志位 总长度
    第1片 0 ~ 1479 1480 0 0 1 1500
    第2片 1480 ~ 2959 1480 1480 185 1 1500
    第3片 2960 ~ 3999 1040 2960 370 0 1060
  3. 重组过程:主机B收到这些分片后,根据标识字段确定它们属于同一个原始数据报,再依据偏移量和MF标志位,按顺序将所有分片拼接起来,最终得到与原始数据报完全相同的4000字节数据。

(二)TCP分段示例

假设主机A要向主机B发送4000字节的应用层数据,网络路径的MTU为1500字节,TCP的MSS协商后为1460字节(即1500 – 20(IP首部) – 20(TCP首部)),IP首部长度为20字节。因为应用层数据大小超过MSS限制,所以需要进行TCP分段。

  1. 计算TCP分段数量:每个TCP数据段的最大有效载荷为1460字节。总数据量为4000字节,因此需要分成4000 ÷ 1460 ≈ 3段。
  2. TCP分段详细信息
    分段编号 序列号范围 数据长度 TCP首部长度 IP首部长度 总长度
    第1段 1 ~ 1460 1460 20 20 1500
    第2段 1461 ~ 2920 1460 20 20 1500
    第3段 2921 ~ 4000 1080 20 20 1120
  3. 重组过程:主机B收到这些分段后,根据TCP序列号将所有分段按顺序拼接起来,得到完整的4000字节数据,并交给应用层。在这个例子中,由于MTU和MSS的设置,无需进行IP分片。

三、IP分片重组的实际实现——以Suricata为例

(一)核心数据结构

Suricata处理IP分片重组时,定义了关键的数据结构:

// defrag.h - 分片重组的主要数据结构
typedef struct DefragTracker_ {
    SCMutex lock;        // 互斥锁保护
    uint32_t id;         // IP包ID
    uint8_t proto;       // IP协议
    uint8_t policy;      // 重组策略
    uint8_t af;          // 地址族(IPv4/IPv6)
    uint8_t seen_last;   // 是否看到最后一个分片
    Address src_addr;    // 源地址
    Address dst_addr;    // 目的地址
    struct IP_FRAGMENTS fragment_tree; // 分片树
} DefragTracker;

// 单个分片的结构
typedef struct Frag_ {
    uint16_t offset;     // 分片偏移 
    uint32_t len;        // 分片长度
    uint8_t more_frags;  // 是否还有更多分片
    uint8_t *pkt;        // 实际分片数据
    RB_ENTRY(Frag_) rb;  // 红黑树节点
} Frag;

DefragTracker用于跟踪同一数据包的所有分片,其中包含互斥锁、IP包ID、协议类型、地址族等信息,还通过分片树管理分片。Frag则定义了单个分片的偏移、长度、是否还有后续分片以及实际数据等内容。

(二)重组流程

decode - ipv4.c文件中,处理流程如下:

int DecodeIPV4(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, const uint8_t *pkt, uint16_t len)
{
    // 1. 检测到分片标记
    if (IPV4_GET_RAW_FRAGOFFSET(ip4h) > 0 || IPV4_GET_RAW_FLAG_MF(ip4h)) {
        // 2. 调用Defrag进行重组
        Packet *rp = Defrag(tv, dtv, p);
        if (rp != NULL) {
            // 3. 重组成功,将重组后的包加入队列
            PacketEnqueueNoLock(&tv->decode_pq, rp);
        }
        p->flags |= PKT_IS_FRAGMENT;
        return TM_ECODE_OK;
    }
}

IPv4通过检查分片偏移和MF标志位识别分片,IPv6则通过分片扩展头识别。Suricata使用红黑树存储各个分片,并按偏移量排序。在Defrag函数中,会创建或查找分片追踪器,将分片加入分片树,检查是否可以重组,重组后生成新包并返回。此外,还有处理IPv6分片头的函数DecodeIPV6FragHeader,用于解析分片头、设置标记和进行合法性检查。

总的来说,Suricata的IP分片重组实现具备以下特点:利用分片跟踪器和分片树管理分片,支持IPv4和IPv6协议,拥有完善的错误检测机制,通过队列管理重组后的包,并且采用互斥锁保证线程安全,全面覆盖了IP分片重组的各个环节。

四、TCP段重组的实际实现——以Zeek为例

(一)核心数据结构

Zeek处理TCP段重组的主要组件是TCP_ReassemblerTCPSessionAdapter,相关数据结构定义如下:

// src/analyzer/protocol/tcp/TCP_Reassembler.h
// TCP_Reassembler类
class TCP_Reassembler final : public Reassembler {
    enum Type {
        Direct,  // 直接传递到目标分析器 
        Forward  // 转发到目标分析器的子节点
    }; 

    TCP_Endpoint* endp;        // TCP端点
    bool had_gap;             // 是否有数据空洞
    bool deliver_tcp_contents; // 是否传递TCP内容
    uint64_t seq_to_skip;     // 要跳过的序列号
    FilePtr record_contents_file; // 记录内容的文件
};

// src/packet_analysis/protocol/tcp/TCPSessionAdapter.h
// TCPSessionAdapter类
class TCPSessionAdapter {
    TCP_Endpoint* orig;         // 发起方端点
    TCP_Endpoint* resp;         // 响应方端点
    bool reassembling;          // 是否正在重组
    bool is_partial;            // 是否部分连接
    uint64_t rel_data_seq;      // 相对数据序列号
};

TCP_Reassembler负责具体的重组工作,TCPSessionAdapter则管理整个TCP会话。

(二)重组流程

当启用重组时,TCPSessionAdapter会创建TCP_Reassembler实例:

// 启用重组:
void TCPSessionAdapter::EnableReassembly() {
    SetReassembler(
        new TCP_Reassembler(this, TCP_Reassembler::Forward, orig),
        new TCP_Reassembler(this, TCP_Reassembler::Forward, resp)
    );
}

当有TCP包到达时,会调用TCP_ReassemblerDataSent函数处理数据:

// 数据到达处理:
// src/analyzer/protocol/tcp/TCP_Reassembler.cc - TCP重组器实现

void TCP_Reassembler::DataSent(double t, uint64_t seq, int len, 
                              const u_char* data) {

    // 检查是否需要跳过
    if (IsSkippedContents(seq, len)) 
        return;

    // 处理数据空洞
    if (seq > last_reassem_seq) {
        Gap(last_reassem_seq, seq - last_reassem_seq);
        had_gap = true;
    }

    // 交付数据
    DeliverBlock(seq, len, data);
}

同时,TCPSessionAdapterProcess函数会获取序列号、处理标志位并进行状态分析和数据重组:

// src/packet_analysis/protocol/tcp/TCPSessionAdapter.cc
void TCPSessionAdapter::Process(bool is_orig, const struct tcphdr* tp,
                              int len, const IP_Hdr* ip) {
    // 获取序列号    
    uint32_t base_seq = ntohl(tp->th_seq);

    // 处理标志位
    TCP_Flags flags(tp);

    // 分析状态
    UpdateStateMachine(t, endpoint, peer, base_seq, flags); 

    // 数据重组
    if (len > 0 && !flags.RST())
        endpoint->DataSent(t, base_seq, len, data);
}

在处理过程中,如果发现序列号不连续,会进行空洞处理:

//  空洞处理:
void TCP_Reassembler::Gap(uint64_t seq, uint64_t len) {
    // 报告空洞
    if (report_gap(endp, endp->peer))
        dst_analyzer->EnqueueConnEvent(content_gap, ...);

    // 更新状态
    had_gap = true;
}

此外,TCPSessionAdapter还会跟踪TCP状态:

// 跟踪TCP状态:
void TCPSessionAdapter::UpdateStateMachine(TCP_Endpoint* endpoint,
    TCP_Endpoint* peer, uint32_t seq, TCP_Flags flags)
{
    // SYN处理
    if (flags.SYN()) {
        if (is_orig) {
            endpoint->SetState(TCP_ENDPOINT_SYN_SENT);
        } else {
            endpoint->SetState(TCP_ENDPOINT_ESTABLISHED); 
        }
    }

    // FIN处理  
    if (flags.FIN()) {
        endpoint->SetState(TCP_ENDPOINT_CLOSED);
    }

    // RST处理
    if (flags.RST()) {
        endpoint->SetState(TCP_ENDPOINT_RESET);
    }
}

Zeek通过序列号跟踪、缓存管理和状态跟踪等机制,确保数据的正确重组。它就像在整理一系列明信片,每张明信片是一个TCP段,通过序列号按顺序排列,遇到乱序或丢失的明信片(数据空洞)时,会等待并处理,最终完成整个“拼图”(数据重组)。

五、简单的TCP流重组Demo示例

// TCP流结构
typedef struct {
    uint32_t src_ip;
    uint32_t dst_ip;
    uint16_t src_port;
    uint16_t dst_port;
    uint32_t next_seq;    // 关键字段:下一个期望的序列号
    uint32_t init_seq;    // 初始序列号
    time_t last_seen;
    unsigned char *stream;  // 存储重组数据的缓冲区
    int stream_size;       // 当前已重组的数据大小
    flow_state state;
    char src_ip_str[INET_ADDRSTRLEN];
    char dst_ip_str[INET_ADDRSTRLEN];
} tcp_flow_t;

// 全局变量
tcp_flow_t flows[MAX_FLOWS];
int flow_count = 0;
void handle_tcp_packet(const struct ip *ip_header, const struct tcphdr *tcp_header,
                       const unsigned char *payload, int payload_len) {
    tcp_flow_t *flow = find_or_create_flow(ip_header, tcp_header);
    if (!flow) return;

    if (tcp_header->th_flags & TH_SYN) {
        if (flow->state == FLOW_NEW) {
            flow->state = FLOW_ESTABLISHED;
            flow->next_seq = ntohl(tcp_header->th_seq) + 1;  // SYN包,序列号+1
            print_flow_info(flow, "SYN");
        }
    }
    else if (tcp_header->th_flags & TH_FIN || tcp_header->th_flags & TH_RST) {
        flow->state = FLOW_CLOSED;
        print_flow_info(flow, tcp_header->th_flags & TH_FIN ? "FIN" : "RST");
        process_stream_data(flow);
    }
    else if (payload_len > 0) {
        uint32_t seq = ntohl(tcp_header->th_seq);
        if (seq == flow->next_seq) {  // 关键重组逻辑:检查序列号是否符合预期
            // 确保不会超出缓冲区
            int copy_len = payload_len;
            if (flow->stream_size + copy_len > MAX_STREAM_SIZE) {
                copy_len = MAX_STREAM_SIZE - flow->stream_size;
            }

            if (copy_len > 0) {
                // 按序复制数据到重组缓冲区
                memcpy(flow->stream + flow->stream_size, payload, copy_len);
                flow->stream_size += copy_len;
                flow->next_seq = seq + copy_len;  // 更新下一个期望的序列号
                flow->state = FLOW_DATA;
                print_flow_info(flow, "DATA");
            }
        }
    }
}

这个Demo实现了基本的TCP流跟踪功能,包含序列号检查、按序重组数据以及状态跟踪(如SYN、FIN、RST等),完整代码可在https://github.com/njcx/pcap_tcp_reassemble获取。

通过对IP分片重组和TCP段重组的深入分析,以及实际示例和代码实现的介绍,希望大家对NIDS开发中的这两个关键环节有更清晰的理解,想必聪明的你已经学会了吧。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/safe/18059.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】