美文网首页
suricata的协议解析的简单分析

suricata的协议解析的简单分析

作者: 明翼 | 来源:发表于2020-02-06 18:14 被阅读0次

    背景

    本文的主要目的分析packet的处理流程,搞清楚一个包处理过程,来看看为什么有的包,在我们的基线的流程中只有一个方向,缺少一个方向的问题。

    一 线程里面核心逻辑

    tm-threads.c 里面的TmThreadsSlotVar 为线程执行的入口函数,worker线程设置如下:

    1. PacketPoolInitEmpty 为线程池初始化。

    2. SCSetThreadName 设置线程名字。

    3. TmThreadSetupOptions 设置线程优先级

    4. TmThreadsSetFlag 设置线程标识

    5. 遍历solt 执行函数进行线程初始化:r = s->SlotThreadInit(tv, s->slot_initdata, &slot_data);

    6. 循环执行 :

      从 队列中获取packet 包信息: p = tv->tmqh_in(tv);

      运行solt里面的插件处理这个包: r = TmThreadsSlotVarRun(tv, p, s);

      处理完毕后放入输出的队列: tv->tmqh_out(tv, p);

      处理solt的post队列的包: slot_post_pq 直到处理完毕。

      这是一个PacketQueue,当SlotFunc被调用以处理数据包时,可能会生成新的需要处理的伪造数据包,插入slot_post_pq队列的伪造数据包将晚于原有数据包进入下一个TmSlot开始处理。当原数据包由tmqh_out交给下一个线程或释放后,才能开始slot_post_pq的处理。目前未看到需要这个队列的场景

    7. 最后处理:

    ​ 打印统计信息

    ​ 设置结束标识

    ​ packet池销毁

    ​ 销毁solt

    ​ 线程退出

    1. worker模式的核心逻辑:

    receive数据包----> Decode数据数据包--->FlowWorker进行包重组流数据处理等--->ResponseReject处理

    二 worker 线程逻辑

    从刚才的第5步执行solt的核心处理逻辑来完成对报文的处理:

    worker线程的核心处理逻辑:

      for (s = slot; s != NULL; s = s->slot_next) {
        TmSlotFunc SlotFunc = SC_ATOMIC_GET(s->SlotFunc);
        PACKET_PROFILING_TMM_START(p, s->tm_id);
         if (unlikely(s->id == 0)) {
        r = SlotFunc(tv, p, SC_ATOMIC_GET(s->slot_data), &s->slot_pre_pq, &s->slot_post_pq);
        } else {
          r = SlotFunc(tv, p, SC_ATOMIC_GET(s->slot_data), &s->slot_pre_pq, NULL);
        }
    

    SoltFunc 进入到:

    static TmEcode FlowWorker(ThreadVars *tv, Packet *p, void *data, PacketQueue *preq, PacketQueue *unused)

    2.1 五元组基线

    FiveTupleBaselineProcess 为新增的五元组处理基线,完成五元组基线的学习和告警工作; 五元组基线的处理逻辑比较简单,为我们自己所写,就不做分析了。

    2.2 Packet包到Flow流

    2.2.1 流数据获取

    FlowHandlePacket(tv, fw->dtv, p); 根据packet找到或新建对应的flow。

    处理过程:

        //获取packet的hash
        const uint32_t hash = p->flow_hash;
        //获取槽位
        FlowBucket *fb = &flow_hash[hash % flow_config.hash_size];
    

    如果槽位为空,则获取一个新的流,注意新流生成要判断下是否超出了内存限制。

    超出了就进入紧急模式,会唤醒flow的管理线程,对flow空闲流和超时流进行内存的回收工作。

    把流的信息初始化为包的信息,包括协议,端口,源的ip,目的ip,ipv4还是ipv6的标识。

    FlowBucket 是flow的一个双向链表,head和tail都指向初始化的流,设置流的状态为:FLOW_STATE_NEW 流的新状态。

    flow是一个两层hash,packet归属哪个流通过源IP,目标IP,源端口和目标端口等判断,判断标准如下:

    #define CMP_FLOW(f1,f2) \
        (((CMP_ADDR(&(f1)->src, &(f2)->src) && \
           CMP_ADDR(&(f1)->dst, &(f2)->dst) && \
           CMP_PORT((f1)->sp, (f2)->sp) && CMP_PORT((f1)->dp, (f2)->dp)) || \
          (CMP_ADDR(&(f1)->src, &(f2)->dst) && \
           CMP_ADDR(&(f1)->dst, &(f2)->src) && \
           CMP_PORT((f1)->sp, (f2)->dp) && CMP_PORT((f1)->dp, (f2)->sp))) && \
         (f1)->proto == (f2)->proto && \
         (f1)->recursion_level == (f2)->recursion_level && \
         (f1)->vlan_id[0] == (f2)->vlan_id[0] && \
         (f1)->vlan_id[1] == (f2)->vlan_id[1])
    

    判断好的流串到FlowBucket的首部。

    2.2.2 包上数据更新

    根据flow信息更新packet上一些信息,比如方向等:

    FlowUpdate调用 FlowHandlePacketUpdate ,做的工作有:

    设置流的时间;

    包的流方向: 如果协议相同,如果packet和flow的端口一致,则是客户端发出的,则为TOSERVER方向;

    如果packet和源端口和目标端口一样,packet的源地址和flow的源地址一样,则是TOSERVER,否则是TOCLIENT方向。

    如果ICMP协议,则直接判断源地址,方向一致就是TOSERVER,反之则是TOCLIENT。

    2.3 TCP包重组

    如果是tcp包,需要进行包的重组,调用:

    StreamTcp(tv, p, fw->stream_thread, &fw->pq, NULL);
    

    重组完成后,要进行包的解析工作,报文分析就是在这里面的。

    1) 在packet的结构体中有归属的flow指针,flow又包含tcp_session数据。

    TcpSession *ssn = (TcpSession *)p->flow->protoctx;

    1. TcpSession是个双向流,包括客户端 和服务器端的: TcpStream

    3) 如果是IPS模式,需要丢包,则进行丢包处理:

        if (StreamTcpCheckFlowDrops(p) == 1) {
            SCLogDebug("This flow/stream triggered a drop rule");
            FlowSetNoPacketInspectionFlag(p->flow);
            DecodeSetNoPacketInspectionFlag(p);
            StreamTcpDisableAppLayer(p->flow);
            PACKET_DROP(p);
            /* return the segments to the pool */
            StreamTcpSessionPktFree(p);
            SCReturnInt(0);
        }
    
    1. 包的重组:

    按照包的方向进行客户端报文重组或服务器端的报文重组,注意包的重组方向是按照packet和flow的方向决定的,

    如果packet是flow的第一个报文,则方向是从客户端对服务器端发的,是客户端方向;以后如果和flow的方向一致的就是客户端方向;

    否则就是服务器端方向。

    StreamTcpReassembleHandleSegment(tv, stt->ra_ctx, ssn,&ssn->client, p, pq);
    

    如果流结束了:PKT_PSEUDO_STREAM_END结束状态,RST重置状态,或是FIN 包,则执行:

    StreamTcpReassembleHandleSegmentUpdateACK 即收到ACK后执行重组,此函数调用:

    StreamTcpReassembleAppLayer(tv, ra_ctx, ssn, stream, p, UPDATE_DIR_OPPOSING) 
    

    判断下stream流,遍历这个流里面的数据段,如果最后一个段为空的,则调用EOF作为结束标识。

        TcpSegment *seg_tail = stream->seg_list_tail;
        //没有片段数据需要处理 
        if (seg_tail == NULL ||
                SEGMENT_BEFORE_OFFSET(stream, seg_tail, STREAM_APP_PROGRESS(stream)))
        {
            /* send an empty EOF msg if we have no segments but TCP state
             * is beyond ESTABLISHED */
            if (ssn->state >= TCP_CLOSING || (p->flags & PKT_PSEUDO_STREAM_END)) {
                SCLogDebug("sending empty eof message");
                /* send EOF to app layer */
                //data数据为NULL,长度为0,结束
                AppLayerHandleTCPData(tv, ra_ctx, p, p->flow, ssn, stream,
                                      NULL, 0,
                                      StreamGetAppLayerFlags(ssn, stream, p, dir));
                AppLayerProfilingStore(ra_ctx->app_tctx, p);
    
                SCReturnInt(0);
            }
        }
    

    正式处理数据流:

    static int ReassembleUpdateAppLayer (ThreadVars *tv,
            TcpReassemblyThreadCtx *ra_ctx,
            TcpSession *ssn, TcpStream *stream,
            Packet *p, enum StreamUpdateDir dir)
    

    从TcpStream中获取缓存数据,然后分析解析:

    // 获取这个流的应用层数据的偏移量offset
    uint64_t app_progress = STREAM_APP_PROGRESS(stream);
        
    while (1) {
            //从 Stream流中获取数据 此处如果只有一个块直接获取,如果多个block根据偏移量获取。
            // 此处数据就是传入协议解析的数据和长度很关键
            // 一般情况mydata_len为offset和stream初始的偏移量之间的差。
            GetAppBuffer(stream, &mydata, &mydata_len, app_progress);
           //此处获取的数据是为空,stream的里面的初始偏移量大于传入的mydata为NULL
            //这里面的长度是stream流的最小偏移量和现在的偏移量的差值。
            if (mydata == NULL && mydata_len > 0 && CheckGap(ssn, stream, p)) {
                SCLogDebug("sending GAP to app-layer (size: %u)", mydata_len);
    
                // 空数据也调用下,不知道为了干嘛?
                int r = AppLayerHandleTCPData(tv, ra_ctx, p, p->flow, ssn, stream,
                        NULL, mydata_len,
                        StreamGetAppLayerFlags(ssn, stream, p, dir)|STREAM_GAP);
                AppLayerProfilingStore(ra_ctx->app_tctx, p);
                
                StreamTcpSetEvent(p, STREAM_REASSEMBLY_SEQ_GAP);
                StatsIncr(tv, ra_ctx->counter_tcp_reass_gap);
                // 把stream的偏移量对后偏移
                stream->app_progress_rel += mydata_len;
                // 这个对后增加的话刚好就可以取到数据了。
                app_progress += mydata_len;
                //如果有空的,不处理了,结束了,我们在解析协议的时候需要判断下
                if (r < 0)
                    break;
    
                continue;
            } else if (mydata == NULL || mydata_len == 0) {
                /* Possibly a gap, but no new data. */
                return 0;
            }
            SCLogDebug("%"PRIu64" got %p/%u", p->pcap_cnt, mydata, mydata_len);
            break;
        }
    
    

    处理tcp协议数据地方在:

        int r = AppLayerHandleTCPData(tv, ra_ctx, p, p->flow, ssn, stream,
                (uint8_t *)mydata, mydata_len,
                StreamGetAppLayerFlags(ssn, stream, p, dir));
        // 处理完一段数据之后,将数据对后移动,下次流就从后面接着对后获取数据。
        stream->app_progress_rel += mydata_len;
    

    获取下协议,协议在流里面已经有标记了,获取办法:

        if (flags & STREAM_TOSERVER) {
            alproto = f->alproto_ts;
        } else {
            alproto = f->alproto_tc;
        }
    

    2.3.1 数据包协议类型分析

    如果知道协议了,则调用协议解析进行解析:

        r = AppLayerParserParse(tv, app_tctx->alp_tctx, f, f->alproto,
                flags, data, data_len);
    

    不知道协议类型,调用:

    //协议类型判断
    TCPProtoDetect(tv, ra_ctx, app_tctx, p, f, ssn, stream,
                               data, data_len, flags) 
                               
    

    调用注册的函数进行判断:

        *alproto = AppLayerProtoDetectGetProto(app_tctx->alpd_tctx,
                f, data, data_len,
                IPPROTO_TCP, flags);
    //调用:
    static AppProto AppLayerProtoDetectPPGetProto(Flow *f,
                                                  uint8_t *buf, uint32_t buflen,
                                                  uint8_t ipproto, uint8_t direction)
    

    主要思路是:

    1. 获取四层协议。

    2. 获取端口信息。

    3. 获取协议,端口解析函数。

    4. 调用解析函数来解析报文,获取协议类型,调用的是类似:

          if (direction & STREAM_TOSERVER && pe->ProbingParserTs != NULL) {
              alproto = pe->ProbingParserTs(f, buf, buflen, NULL);
          } else if (pe->ProbingParserTc != NULL) {
              alproto = pe->ProbingParserTc(f, buf, buflen, NULL);
          }
      

      最终调用:IEC104ProbingParser 这种解析类,这样就获取到具体的应用层协议了。

    2.3.2 数据包的具体协议还原分析

    解析调用的解析函数指针如下:

    AppLayerParserProtoCtx *p = &alp_ctx.ctxs[f->protomap][alproto];
    // server or client 两个方向的函数指针,调用解析
    p->Parser[(flags & STREAM_TOSERVER) ? 0 : 1](f, alstate, pstate,
                    input, input_len,
                    alp_tctx->alproto_local_storage[f->protomap][alproto])
    

    最终调用的函数类似于

    static int IEC104ParseRequest(Flow *f, void *state,    AppLayerParserState *pstate, uint8_t *input, uint32_t input_len,
        void *local_data)
    

    至此,协议的解析过程分析完毕。

    相关文章

      网友评论

          本文标题:suricata的协议解析的简单分析

          本文链接:https://www.haomeiwen.com/subject/agpuxhtx.html