suricata的协议解析的简单分析

2020-02-06  本文已影响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)

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

上一篇下一篇

猜你喜欢

热点阅读