suricata的协议解析的简单分析
背景
本文的主要目的分析packet的处理流程,搞清楚一个包处理过程,来看看为什么有的包,在我们的基线的流程中只有一个方向,缺少一个方向的问题。
一 线程里面核心逻辑
tm-threads.c 里面的TmThreadsSlotVar 为线程执行的入口函数,worker线程设置如下:
-
PacketPoolInitEmpty 为线程池初始化。
-
SCSetThreadName 设置线程名字。
-
TmThreadSetupOptions 设置线程优先级
-
TmThreadsSetFlag 设置线程标识
-
遍历solt 执行函数进行线程初始化:
r = s->SlotThreadInit(tv, s->slot_initdata, &slot_data);
-
循环执行 :
从 队列中获取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的处理。目前未看到需要这个队列的场景
-
最后处理:
打印统计信息
设置结束标识
packet池销毁
销毁solt
线程退出
- 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;
- 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);
}
- 包的重组:
按照包的方向进行客户端报文重组或服务器端的报文重组,注意包的重组方向是按照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)
主要思路是:
-
获取四层协议。
-
获取端口信息。
-
获取协议,端口解析函数。
-
调用解析函数来解析报文,获取协议类型,调用的是类似:
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)
至此,协议的解析过程分析完毕。