suricata学习

suricata简介

Suricata是一个高性能的网络IDS,IPS和网络安全监控引擎。

IPS:入侵预防系统(IPS: Intrusion Prevention System)是电脑网络安全设施,是对防病毒软件(Antivirus Programs)和防火墙(Packet Filter, Application Gateway)的补充。 入侵预防系统(Intrusion-prevention system)是一部能够监视网络或网络设备的网络资料传输行为的计算机网络安全设备,能够即时的中断、调整或隔离一些不正常或是具有伤害性的网络资料传输行为。是新一代的侵入检测系统(IDS)。

 

Suricata是一个网络入侵检测防护引擎,由开放信息安全基金会及其支持的厂商开发。该引擎是多线程的,内置支持IPV6。可加载现有的Snort规则和签名,支持Barnyard  Barnyard2 工具.

 

IDS英文“Intrusion Detection Systems”的缩写,中文意思是“入侵检测系统”。依照一定的安全策略,通过软、硬件,对网络、系统的运行状况进行监视,尽可能发现各种攻击企图、攻击行为或者攻击结果,以保证网络系统资源的机密性、完整性和可用性。

 

Barnyard知名的开源IDS的日志工具,具有快速的响应速度,优异的数据库写入功能,是做自定义的入侵检测系统不可缺少的插件。


至于IDS和IPS的区别,可以查看下网络中其他文章,本人理解:

IDS只是发现攻击、产生报警,而IPS不但可以发现攻击,更重要的是针对攻击采取行动。


<suricata编译安装>

想到研究suricata只读源码估计还不凑效,需要了解下真实环境下怎么应用,这样理解起来估计会更有感觉,于是在自己本地虚拟机中安装编译一下:

1、虚拟机操作系统Linux centos5.0
2、下载suricata源码
   下载yaml库,主要用于配置文件操作

   由于自己本机原来安装配置过snort,所以suricata+yaml库已经可以正常运行。
   否则需要安装:libdnet-1.12.tgz、libpcap-1.1.1.tar.gz、pcre-8.32.tar.gz等库。

3、安装yaml库
   解压:tar -zxvf yaml-0.1.4.tar.gz到特定目录
   ./configure
   make
   make install
   
4、安装suricata
   解码:tar -zxvf suricata-1.4.7.tar.gz 到特定目录
   ./configure
   make 
   make install
5、以上完成编译安装,接下来需要更改配置
   suricata默认安装路径:/usr/local/bin
   创建默认配置目录:mkdir -p /usr/local/etc/suricata
   拷贝配置文件:cp 源码目录中 suricata.yaml /usr/local/etc/suricata
                 cp 源码目录中 reference.config /usr/local/etc/suricata
                 cp 源码目录中 classification.config /usr/local/etc/suricata
                 cp 源码目录中 threshold.config /usr/local/etc/suricata
   创建默认日志目录:mkdir -p /usr/local/var/log/suricata
6、下载规则库
   解压到/usr/local/etc/suricata目录下
  
7、默认启动:./suricata -i eth0 (eth0需要根据本地网卡配置)
   这时suricata正常启动,如下图所示:
   

当只执行./suricata时显示如下:


=================================================================================
以上是编译安装的整个过程,为了更好的理解suricata的功能,自己做了一个简单的测试:
1、自己编写一条规则,规则书写参考snort规则(suricata完全兼容snort规则)
   例如以百度网站为例:
   alert http any any -> any any (msg:"hit baidu.com..."; reference:url, www.baidu.com;)
   将文件命名为test.rules,存放在目录/usr/local/etc/suricata下(直接存放在该目录下rules里面也可以,这里为了说明,自己单独存放)。
2、启动suricata 
  ./suricata  -s /usr/local/etc/suricata/test.rules -i eth0

3、打开虚拟机中浏览器,访问www.baidu.com
   此时,查看log文件/user/local/var/log/suricata/目录下
   fast.log显示数据包匹配的条数
   http.log (目前不清楚内容含义)
   unified2.alert.13XXXXXXXX (目前不清楚内容含义)

目前只研究到这里,内容等待后续完善。

suricata源码阅读main函数开始

从官方网站上查看到资料,目前最新的稳定版本为1.4.7,测试版本最新为2.0beta2
http://www.openinfosecfoundation.org/download/suricata-2.0beta2.tar.gz
suricata目前正处在开发阶段,版本更新的比较频繁,为此选择一个稳定版本来分析。网络中中文资料相对较少,介于自己英文水平soso,还是决定直接阅读源码。
开源中国中blog文章可以拿来参考:
http://my.oschina.net/openadrian/blog/184621

目前分析版本suricata-1.4.7:
suricata.c文件中包含程序主函数main(),该函数一共有1K多行代码,阅读起来真是费劲,难道作者就不能封装下吗?我表示费劲…… 废话少说,直接深入。
1、开头定义的变量直接略过。。。

2、 sc_set_caps = FALSE; 
    int sc_set_caps;
    #define TRUE   1
    #define FALSE  0
    标识是否对主线程进行特权去除(drop privilege),主要是出于安全性考虑。
       (目前自己还不知道是什么作用,出自上面的文章)

3、 SC_ATOMIC_INIT(engine_stage);
    初始化原子变量engine_stage –> 记录程序当前的运行阶段:SURICATA_INIT、SURICATA_RUNTIME、SURICATA_FINALIZE。
    #define SC_ATOMIC_INIT(name) \
        (name ## _sc_atomic__) = 0
     上述这个宏定义中的操作时什么意思呢?目前不解,后期再询问。

4、 SCLogInitLogModule(NULL);
    初始化日志模块,因为后续的执行流程中将使用日志输出,所以需要最先初始化该模块。

5、SCSetThreadName("Suricata-Main") 
    设置当前主线程名字为“Suricata-Main”。线程名字还是挺重要的,至少在gdb调试时info threads可以看到各个线程名,从而可以精确地找到想要查看的线程。另外,在top -H时,也能够显示出线程名字(然而ps -efL时貌似还是只是显示进程名)。

6、 RunModeRegisterRunModes();具体详见本人另一篇《suricata中模式概念详解 》
   注册各种运行模式。Suricata对“运行模式”这个概念也进行了封装。运行模式存储在runmodes数组中,定义为RunModes runmodes[RUNMODE_USER_MAX]。
首先,数组中每一项(例如runmodes[RUNMODE_PCAP_DEV]),对应一组运行模式,模式组包括(RunModes类型):“IDS+Pcap”模式组、“File+Pcap”模式组、“UnixSocket”模式组等(另外还有其他一些内部模式,如:“列出关键字”模式、“打印版本号”模式等,这些没有存储在runmodes数组中)。
然后,每一个模式组,其中可以包含若干个运行模式(RunMode类型),例如:single、auto、autofp、workers。
运行模式的注册,则是为各个模式组(如RunModeIdsPcapRegister)添加其所支持的运行模式(通过调用RunModeRegisterNewRunMode),并定义改组的默认运行模式,以及非常重要的:注册各个模式下的初始化函数(如RunModeIdsPcapSingle),等后续初始化阶段确定了具体的运行模式后,就会调用这里注册的对应的初始化函数,对该模式下的运行环境进行进一步配置。

7、 SET_ENGINE_MODE_IDS(engine_mode);
   初始化引擎模式为IDS模式。引擎模式只有两种:IDS、IPS,初始默认为IDS,而在nfq或ipfw启用时,就会切换成IPS模式,该模式下能够执行“Drop”操作,即拦截数据包。

8、 ConfInit();
   初始化配置模块。为配置节点树建立root节点。

9、以下是命令行参数解析,具体调用getopt_long()函数实现(函数具体实现参加本人另外一篇博文《getopt_long()解析命令行选项参数》)。感觉这里是理解的一个重点地方,首先理解命令行都有哪些参数,每个参数的大概作用是什么,进而调用的函数关系如何,所以本人提取在网上查找了下suricata命令行参数的文章阅读了一下,具体参加本人博文《suricata命令行》。
   这里的代码很长:
   while ((opt = getopt_long(argc, argv, short_opts, long_opts, &option_index)) != -1) {
   ……
// 这个里面主要是解析命令行参数,解析完后操作
        pcap_dev = "eth0";           // 这里是启动时参数,例如:./suricata -i eth0     
        run_mode = RUNMODE_PCAP_DEV; // 设置运行模式
        LiveRegisterDevice(pcap_dev); // 添加pcap设备监控,作用目前还不清楚
   }
   其中,与包捕获相关的选项(如“-i”)都会调用LiveRegisterDevice,以注册一个数据包捕获设备接口(如eth0)。全局的所有已注册的设备接口存储在变量live_devices中,类型为LiveDevice。注意,用多设备同时捕获数据包这个特性在Suricata中目前还只是实验性的。“-v”选项可多次使用,每个v都能将当前日志等级提升一级。
   SetBpfString(optind, argv); // 解析BPF过滤器
10、 UtilCpuPrintSummary();// 打印cpu信息

11、 CheckValidDaemonModes(daemon, run_mode)
若运行模式为内部模式daemon,则进入该模式执行,完毕后退出程序。

12、初始化全局变量、队列、时间等

13、 SupportFastPatternForSigMatchTypes();
为快速模式匹配注册关键字。调用SupportFastPatternForSigMatchList函数,按照优先级大小插入到sm_fp_support_smlist_list链表中。

14、MpmTableSetup(); // 设置多模式匹配表
该表中每一项就是一个实现了某种多模式匹配算法(如WuManber、AC)的匹配器。以注册AC匹配器为例,MpmTableSetup会调用MpmACRegister函数实现AC注册,函数内部其实只是填充mpm_table中对应AC的那一项(mpm_table[MPM_AC])的各个字段,如:匹配器名称("ac")、初始化函数(SCACInitCtx)、增加模式函数(SCACAddPatternCS)、实际的搜索执行函数(SCACSearch)。

15、如果用户没有设置配置文件,使用默认配置文件suricata.yaml

16、加载配置文件yaml格式
调用LoadYamlConfig读取Yaml格式配置文件。Yaml格式解析是通过libyaml库来完成的,解析的结果存储在配置节点树(见conf.c)中。对include机制的支持:在第一遍调用ConfYamlLoadFile载入主配置文件后,将在当前配置节点树中搜寻“include”节点,并对其每个子节点的值(即通过include语句所指定的子配置文件路径),同样调用ConfYamlLoadFile进行载入。

17、AppLayerDetectProtoThreadInit();
初始化应用层协议检测模块。其中,AlpProtoInit函数初始化该模块所用到的多模式匹配器,RegisterAppLayerParsers函数注册各种应用层协议的解析器(如RegisterHTPParsers函数对应HTTP协议),而AlpProtoFinalizeGlobal函数完成一些收尾工作,包括调用匹配器的预处理(Prepare)函数、建立模式ID和规则签名之间的映射等。

18、 AppLayerParsersInitPostProcess();// 建立了一个解析器之间的映射

19、设置并验证日志目录是否存在

20、 获取与包捕获相关的一些配置参数

21、SCHInfoLoadFromConfig()
从配置文件中载入host os policy(主机OS策略)信息。网络入侵通常是针对某些特定OS的漏洞,因此如果能够获取部署环境中主机的OS信息,肯定对入侵检测大有裨益。具体这些信息是怎么使用的,暂时也还不清楚。

22、DefragInit()
初始化IP分片重组模块。

23、SigTableSetu()
初始化检测引擎,主要是注册检测引擎所支持的规则格式(跟Snort规则基本一致)中的关键字,比如sid、priority、msg、within、distance等等。

24、TmqhSetup()
初始化queue handler(队列处理函数),这个是衔接线程模块和数据包队列之间的桥梁,目前共有5类handler:simple, nfq, packetpool, flow, ringbuffer。每类handler内部都有一个InHandler和OutHandler,一个用于从上一级队列中获取数据包,另一个用于处理完毕后将数据包送入下一级队列。

25、接下来几个函数
StorageInit:初始化存储模块,这个模块可以用来临时存储一些数据,数据类型目前有两种:host、flow。具体在何种场景下用,目前未知。
CIDRInit:初始化CIDR掩码数组,cidrs[i]对应前i位为1的掩码。
SigParsePrepare:为规则签名解析器的正则表达式进行编译(pcre_compile)和预处理(pcre_study)。
SCPerfInitCounterApi:初始化性能计数器模块。这个模块实现了累加计数器(例如统计收到的数据包个数、字节数)、平均值计数器(统计平均包长、处理时间)、最大计数器(最大包长、处理时间)、基于时间间隔的计数器(当前流量速率)等,默认输出到日志目录下的stats.log文件。
几个Profiling模块的初始化函数。Profiling模块提供内建的模块性能分析功能,可以用来分析模块性能、各种锁的实际使用情况(竞争时间)、规则的性能等。
SCReputationInitCtx:初始化IP声望模块。IP声望数据在内部是以Radix tree的形式存储的,但目前还不知道数据源是从哪来的,而且也没看到这个模块的函数在哪调用。
SCProtoNameInit:读取/etc/protocols文件,建立IP层所承载的上层协议号和协议名的映射(如6-> ”TCP”,17-> ”UDP“)。
TagInitCtx、ThresholdInit:与规则中的tag、threshould关键字的实现相关,这里用到了Storage模块,调用HostStorageRegister和FlowStorageRegister注册了几个(与流/主机绑定的?)存储区域。
DetectAddressTestConfVars、DetectPortTestConfVars:检查配置文件中"vars"选项下所预定义的一些IP地址(如局域网地址块)、端口变量(如HTTP端口号)是否符合格式要求。

26、注册模块
注册Suricata所支持的所有线程模块(Thread Module)。
以pcap相关模块为例,TmModuleReceivePcapRegister函数注册了Pcap捕获模块,而TmModuleDecodePcapRegister函数注册了Pcap数据包解码模块。所谓注册,就是在tmm_modules模块数组中对应的那项中填充TmModule结构的所有字段,这些字段包括:模块名字、线程初始化函数、包处理或包获取函数、线程退出清理函数、一些标志位等等。

27、 AppLayerHtpNeedFileInspection()
设置suricata内部模块与libhtp(HTTP处理库)对接关系的函数,具体细节暂时不管。

28、DetectEngineRegisterAppInspectionEngines()
作用不详

29、注册规则加载信号函数
若设置了rule_reload标志,则注册相应的信号处理函数(目前设置的函数都是些提示函数,没有做实际重载)。这里用的是比较惯用的SIGUSR2信号来触发rule reload。

30、TmModuleRunInit()
调用之前注册的线程模块的初始化函数进行初始化。

31、检查是否进入Daemon模式
若需要进入Daemon模式,则会检测pidfile是否已经存在(daemon下只能有一个实例运行),然后进行Daemonize,最后创建一个pidfile。Daemonize的主要思路是:fork->子进程调用setsid创建一个新的session,关闭stdin、stdout、stderr,并告诉父进程 –> 父进程等待子进程通知,然后退出 –> 子进程继续执行。

32、注册信号
    UtilSignalHandlerSetup(SIGINT, SignalHandlerSigint);
    UtilSignalHandlerSetup(SIGTERM, SignalHandlerSigterm);
    UtilSignalHandlerSetup(SIGPIPE, SIG_IGN);
    UtilSignalHandlerSetup(SIGSYS, SIG_IGN);
首先为SIGINT(ctrl-c触发)和SIGTERM(不带参数kill时触发)这两个常规退出信号分别注册handler,对SIGINT的处理是设置程序的状态标志为STOP,即让程序优雅地退出;而对SIGTERM是设置为KILL,即强杀。接着,程序会忽略SIGPIPE(这个信号通常是在Socket通信时向已关闭的连接另一端发送数据时收到)和SIGSYS(当进程尝试执行一个不存在的系统调用时收到)信号,以加强程序的容错性和健壮性。

33、获取配置文件中指定的Suricata运行时的user和group
如果命令行中没有指定的话。然后,将指定的user和group通过getpwuid、getpwnam、getgrnam等函数转换为uid和gid,为后续的实际设置uid和gid做准备。

34、PacketPoolInit(max_pending_packets)
初始化Packet pool,即预分配一些Packet结构体,分配的数目由之前配置的max_pending_packets确定,而数据包的数据大小由default_packet_size确定(一个包的总占用空间为default_packet_size+sizeof(Packet))。在调用PacketGetFromAlloc新建并初始化一个数据包后,再调用PacketPoolStorePacket将该数据包存入ringbuffer。Suricata中用于数据包池的Ring Buffer类型为RingBuffer16,即容量为2^16=65536(但为什么max_pending_packets的最大值被限定为65534呢?)。

35、HostInitConfig(HOST_VERBOSE)

36、 FlowInitConfig(FLOW_VERBOSE)
初始化Flow engine。跟前面的host engine类似,不过这个的用处就很明显了,就是用来表示一条TCP/UDP/ICMP/SCTP流的,程序当前所记录的所有流便组成了流表,在flow引擎中,流表为flow_hash这个全局变量,其类型为FlowBucket *,而FlowBucket中则能够存储一个Flow链表,典型的一张chained hash Table。在初始化函数FlowInitConfig中,首先会使用配置文件信息填充flow_config,然后会按照配置中的hash_size为流表实际分配内存,接着按照prealloc进行流的预分配(FlowAlloc->FlowEnqueue,存储在flow_spare_q这个FlowQueue类型的队列中),最后调用FlowInitFlowProto为流表所用于的各种流协议进行配置,主要是设置timeout时间。

37、DetectEngineCtxInit()
初始化Decect engine。若配置文件中未指定mpm(多模式匹配器),则默认使用AC,即使用mpm_table中AC那一项。SRepInit函数(与前面的SCReputationInitCtx不同!)会初始化检测引擎中域reputaion相关信息,即从配置文件中指定的文件中读取声望数据。其余配置比较复杂,暂不关注。

38、读取和解析classification.config和reference.config文件
SCClassConfLoadClassficationConfigFile(de_ctx);
SCRConfLoadReferenceConfigFile(de_ctx);
这两个文件用于支持规则格式中的classification(规则分类)和refercence(规则参考资料)字段。

39、ActionInitConfig()
设置规则的动作优先级顺序,默认为Pass->Drop->Reject->Alert。举例来说,若有一条Pass规则和Drop规则都匹配到了某个数据库,则会优先应用Pass规则。

40、MagicInit()
初始化Magic模块。Magic模块只是对libmagic库进行了一层封装,通过文件中的magic字段来检测文件的类型(如”PDF-1.3“对应PDF文件)。

41、接下来貌似和延迟有关
设置是否延迟检测。若delayed-detect为yes,则系统将在载入规则集之前就开始处理数据包,这样能够在IPS模式下将少系统的down time(宕机时间)。
如果没有设置延迟检测,就调用LoadSignatures载入规则集。
如果设置了live_reload,则重新注册用于规则重载的SIGUSR2信号处理函数(这次是设置为真正的重载处理函数)。放在这里是为了防止在初次载入规则集时就被触发重载。

41、SCAsn1LoadConfig()
初始化ASN.1解码模块。Wikipedia:ASN.1(Abstract Syntax Notation One) 是一套标准,是描述数据的表示、编码、传输、解码的灵活的记法。应用层协议如X.400(email)、X.500和LDAP(目录服务)、H.323(VoIP)和SNMP使用 ASN.1 描述它们交互的协议数据单元。

42、 CoredumpLoadConfig()
处理CoreDump相关配置。Linux下可用prctl函数获取和设置进程dumpable状态,设置corefile大小则是通过通用的setrlimit函数。

43、gettimeofday(&start_time, NULL)
调用gettimeofday保存当前时间,存储在suri->start_time中,作为系统的启动时间。

44、SCDropMainThreadCaps(userid, groupid)
去除主线程的权限。这个是通过libcap-ng实现的,首先调用capng_clear清空所有权限,然后根据运行模式添加一些必要权限(主要是为了抓包),最后调用capng_change_id设置新的uid和gid。主线程的权限应该会被新建的子线程继承,因此只需要在主线程设置即可。

45、RunModeInitializeOutputs()
初始化所有Output模块。这些模块之前在线程模块注册函数里已经都注册了,这里会根据配置文件再进行配置和初始化,最后把当前配置下启用了的output模块放到RunModeOutputs链表中。

46、若当前抓包模式下未指定设备接口(通过-i 或--pcap=等方式),则解析配置文件中指定的Interface,并调用LiveRegisterDevice对其进行注册。

47、if(conf_test == 1)
若当前的模式为CONF_TEST,即测试配置文件是否有效,则现在就可以退出了。这也说明,程序运行到这里,配置工作已经基本完成了。

48、RunModeDispatch(run_mode, runmode_custom_mode, de_ctx)
初始化运行模式。首先,根据配置文件和程序中的默认值来配置运行模式(single、auto这些),而运行模式类型(PCAP_DEV、PCAPFILE这些)也在之前已经确定了,因此运行模式已经固定下来,可以从runmodes表中获取到特定的RunMode了,接着就调用RunMode中的RunModeFunc,进入当前运行模式的初始化函数。以PCAP_DEV类型下的autofp模式为例,该模式的初始化函数为:RunModeIdsPcapAutoFp。这个函数的执行流程为:
调用RunModeInitialize进行通用的运行模式初始化,目前主要是设置CPU affinity和threading_detect_ratio。
调用RunModeSetLiveCaptureAutoFp设置该模式下的模块组合:
确实参数:接口个数nlive、线程个数thread_max(由用户指定,或CPU个数决定)。
RunmodeAutoFpCreatePickupQueuesString:创建一个包含thread_max个接收队列名字的字符串,如"pickup1,pickup2,pickup3"。
ParsePcapConfig:解析pcap接口相关配置,如buffer-size、bpf-filter、promisc等。
PcapConfigGeThreadsCount:获取pcap接口配置中指定的threads(抓包线程个数,默认为1),保存到threads_count变量。
创造threads_count个抓包线程:
TmThreadCreatePacketHandler函数专门用于创建包处理线程,函数内部会调用通用的TmThreadCreate创建线程,并将线程类型设置为TVT_PPT。
线程名字为"RxPcap"+接口名+i,如“RxPcapeth01”。
inq、inqh都设置为"packetpool",表示将从数据包池(而不是某个数据包队列)中获取包。
outqh设置为"flow",表示使用之前注册的flow类型的queue handler作为线程的输出队列处理器,这个类型可以保证同一条flow的包都会输出给同一个queue,具体的包调度策略取决于autop-scheduler指定的算法。
outq设置为前面所设置的接收队列名字符串,而之前的flow类型handler的TmqhOutputFlowSetupCtx函数将会解析队列名字符串,并创建出相应个数(threads_max)的队列。
slots函数设置为"pktacqloop",表示这个线程的插槽类型为pktacqloop,这样在TmThreadSetSlots函数中就会将线程执行函数(tm_func)设置为针对该插槽类型的TmThreadsSlotPktAcqLoop函数。最终线程在被pthread_create执行时传入的回调函数就是这个线程执行函数。
TmSlotSetFuncAppend:将“ReceivePcap"和"DecodePcap"这两个线程模块嵌入到前面创建的每个抓包线程的插槽中去。
TmThreadSetCPU:设置线程的CPU相关参数。
TmThreadSpawn:按照之前所填充好的ThreadVars生成实际的线程,并将该线程添加到全局线程数组tv_root中去。
创造thread_max个检测线程:
线程名字为"Detect"+i,每个线程都有与一个输入队列绑定,即inq设置为"pickup"+i 队列。
inqh设置为"flow",即使用flow类型(与前面的抓包线程相匹配)的queue handler作为线程的输入队列处理器。
outq、outqh都设置为"packetpool",表示这个线程的包处理完后会直接回收到packet pool中去。
slots函数设置为"varslot",表示这个线程的插槽类型为varslot,对应的执行函数为TmThreadsSlotVar。
接着,跟上面类似,把"StreamTcp"(用于TCP会话跟踪、重组等)、"Detect"(调用检测引擎进行实际的入侵检测)和"RespondReject"(用于通过主动应答来拒绝连接)这三个线程模块嵌入进去。不过,这里在插入“Detect”模块时,调用的是TmSlotSetFuncAppendDelayed,用于支持delayed-detect功能。
SetupOutputs:由于这组检测线程是处理数据包的完结之处,因此这里需要把输出模块也嵌入到这些线程中去,方式也是通过TmSlotSetFuncAppend函数,对象是RunModeOutputs中存储的输出模块。

49、若unix-command为enable状态,则创建Unix-socket命令线程,可与suricata客户端使用JSON格式信息进行通信。命令线程的创建是通过TmThreadCreateCmdThread函数,创建的线程类型为TVT_CMD。线程执行函数为UnixManagerThread。

50、FlowManagerThreadSpawn()
创建Flow管理线程,用于对流表进行超时删除处理。管理线程创建是通过TmThreadCreateMgmtThread函数,类型为TVT_MGMT,执行函数为FlowManagerThread。

51、StreamTcpInitConfig(STREAM_VERBOSE)
初始化Stream TCP模块。其中调用了StreamTcpReassembleInit函数进行重组模块初始化。

52、 SCPerfSpawnThreads()
创建性能计数相关线程,包括一个定期对各计数器进行同步的唤醒线程(SCPerfWakeupThread),和一个定期输出计数值的管理线程(SCPerfMgmtThread)。

53、TmValidateQueueState()
检查数据包队列的状态是否有效:每个数据包队列都应该至少有一个reader和一个writer。在前面线程绑定inq时会增加其reader_cnt,绑定outq时会增加其writer_cnt。

54、TmThreadWaitOnThreadInit()
等待子线程初始化完成。检查是否初始化完成的方式是遍历tv_root,调用TmThreadsCheckFlag检查子线程的状态标志。

55、SC_ATOMIC_CAS(&engine_stage, SURICATA_INIT, SURICATA_RUNTIME)
更新engine_stage为SURICATA_RUNTIME,即程序已经初始化完成,进入运转状态。这里的更新用的是原子CAS操作,防止并发更新导致状态不一致(但目前没在代码中只到到主线程有更新engine_stage操作,不存在并发更新)。

56、TmThreadContinueThreads()
让目前处于paused状态的线程继续执行。在TmThreadCreate中,线程的初始状态设置为了PAUSE,因此初始化完成后就会等待主线程调用TmThreadContinue让其继续。从这以后,各线程就开始正式执行其主流程了。

57、if (delayed_detect)
若设置了delayed_detect,则现在开始调用LoadSignatures加载规则集,激活检测线程,并注册rule_reload信号处理函数。这里,激活检测线程是通过调用TmThreadActivateDummySlot函数,这个函数会将之前注册的slot中的slotFunc替换为实际操作函数,而不是原先在delayed_detect情况下设置的什么都不做的TmDummyFunc。

58、进入死循环
 while(1) {
        if (suricata_ctl_flags & (SURICATA_KILL | SURICATA_STOP)) {
            SCLogInfo("Signal Received.  Stopping engine.");

            break;
        }

        TmThreadCheckThreadState();

        usleep(10* 1000);
    }
若受到引擎退出信号(SURICATA_KILL或SURICATA_STOP),则退出循环,执行后续退出操作,否则就调用TmThreadCheckThreadState检查各线程的状态,决定是否进行结束线程、重启线程、终止程序等操作,然后usleep一会儿(1s),继续循环。

59、退出阶段
首先会更新engine_stage为SURICATA_DEINIT,然后依次关闭Unix-socket线程、Flow管理线程。

停止包含抓包或解码线程模块的线程。这个是通过TmThreadDisableThreadsWithTMS实现,里面会检查每个线程的slots里嵌入的线程模块的flags中是否包含指定的flag(这里是TM_FLAG_RECEIVE_TM或TM_FLAG_DECODE_TM),一个线程模块的flags在注册时就已经指定了。关闭是通过向线程发送KILL信号(设置线程变量的THV_KILL标志)实现,收到该信号的线程会进入RUNNING_DONE状态,然后等待主线程下一步发出DEINIT信号。

强制对仍有未处理的分段的流进行重组。

打印进程运行的总时间(elapsed time)。

在rule_reload开启下,首先同样调用TmThreadDisableThreadsWithTMS停止检测线程。特别地,该函数对于inq不为"packetpool"的线程(即该线程从一个PakcetQueue中获取数据包),会等到inq中的数据包都处理完毕再关闭这个线程。然后,检测是否reload正在进行,如果是则等待其完成,即不去打断它。

杀死所有子线程。杀死线程的函数为TmThreadKillThread,这个函数会同时向子线程发出KILL和DEINIT信号,然后等待子线程进入CLOSED状态,之后,再调用线程的清理函数(InShutdownHandler)以及其对应的ouqh的清理函数(OutHandlerCtxFree),最后调用pthread_join等待子线程退出。

执行一大堆清理函数:清理性能计数模块、关闭Flow engine、清理StreamTCP、关闭Host engine、清理HTP模块并打印状态、移除PID文件、关闭检测引擎、清理应用层识别模块、清理Tag环境、关闭所有输出模块,etc…

调用exit以engine_retval为退出状态终止程序。


suricata中模式概念详解
截止目前,结合开源中国中“揹着笔记本流浪”的blog中文章,已经对main函数和数据包收取、解码有了初步了解。其中遇到的困难和疑惑的地方记录在这里,便于今后学习和提升。
1、网络编程知识经验不足,suricata整个架构采用多线程编程的方式,如若不了解线程的用法,理解起来会存在一些难度。
2、底层网络协议了解不够,需要日后补充下知识,感觉仅限了解的层面即可,至少可以看懂。
3、在收包过程中需要熟悉libpcap库的接口和用法。

今天根据前人的blog和源码的研读,对于数据包收取、解析等整个过程大致走了一遍,疑惑的地方记录如下:
1、suricata中用到大量的注册函数,至于其中精髓还不曾体会的到,目前只停留在表层能够读懂的阶段。
2、一个slot概念(“槽”),貌似不理解这个会对阅读代码产生一定影响。自己目前感觉就是为了注册数据包处理流程中的函数,具体用法日后有体会了再补充。
3、现在最大的一个疑惑就是对于“模式”概念的迷惑,suricata中貌似运行模式中又会细分各种模式,废话少说,还是停下来小结一下,便于自己理解。
● main()中调用RunModeRegisterRunModes()

● RunModeRegisterRunModes()函数中注册各种运行模式
   RunModeIdsPcapRegister();      // IDS+pcap
   RunModeFilePcapRegister();     // File+pcap
   RunModeIdsPfringRegister();    // IDS+pfring
   RunModeIpsIPFWRegister();      // IPS+ipfw
   RunModeIpsNFQRegister();       // IPS+nfq
   RunModeErfFileRegister();      // erf+file
   RunModeErfDagRegister();       // erf+dag
   RunModeNapatechRegister();     // napatech
   RunModeIdsAFPRegister();       // IDS+AFP
   RunModeUnixSocketRegister();   // UnixSocket
  
   这些运行模式存储在runmodes数组中,定义如下:
   static RunModes runmodes[RUNMODE_MAX];
  
   其中数组大小RUNMODE_MAX定义为如下枚举类型:
 
  enum {
    RUNMODE_UNKNOWN = 0,
    RUNMODE_PCAP_DEV,
    RUNMODE_PCAP_FILE,
    RUNMODE_PFRING,
    RUNMODE_NFQ,
    RUNMODE_IPFW,
    RUNMODE_ERF_FILE,
    RUNMODE_DAG,
    RUNMODE_AFP_DEV,
    RUNMODE_UNITTEST,
    RUNMODE_NAPATECH,
    RUNMODE_UNIX_SOCKET,
    RUNMODE_MAX,
  };
  
● 下面以pcap模式为例子说明注册操作内容
   RunModeIdsPcapRegister()函数注册IDS+pcap模式,其余模式类似。
   具体内容如下:
   RunModeRegisterNewRunMode(RUNMODE_PCAP_DEV, "single",
                            "Single threaded pcap live mode",
                            RunModeIdsPcapSingle);
   RunModeRegisterNewRunMode(RUNMODE_PCAP_DEV, "auto",
                            "Multi threaded pcap live mode",
                            RunModeIdsPcapAuto);
   RunModeRegisterNewRunMode(RUNMODE_PCAP_DEV, "autofp",
                            "Multi threaded pcap live mode.  Packets from "
                            "each flow are assigned to a single detect thread, "
                            "unlike "pcap_live_auto" where packets from "
                            "the same flow can be processed by any detect "
                            "thread",
                            RunModeIdsPcapAutoFp);
   RunModeRegisterNewRunMode(RUNMODE_PCAP_DEV, "workers",
                              "Workers pcap live mode, each thread does all"
                              " tasks from acquisition to logging",
                              RunModeIdsPcapWorkers);
   暂且不管这里函数作用,但这里可以看出运行模式又细分为四类:
   single、auto、autofp、workers
  
● 这里具体看一下RunModeRegisterNewRunMode()函数作用
   函数原型:
   void RunModeRegisterNewRunMode(int runmode, const char *name,
                                 const char *description,
                                 int (*RunModeFunc)(DetectEngineCtx *))
   函数作用:注册一个新的运行模式
   函数参数:runmode是运行模式类型,对应于上面的枚举类型定义,例如:RUNMODE_PCAP_DEV。
             name是每一种运行模式下面的自定义模式,为每一种运行模式的主键。
             description是运行模式的描述。
             RunModeFunc是运行该模式的具体函数。
 
● 下面回过头看一下RunModeIdsPcapRegister()函数中的四类自定义模式:
   single : 单线程pcap live模式
   auto   :多线程pcap live模式
   autofp :多线程pcap live模式,但每个流的数据包被分配到一个单一的detect线程(检测线程)。
            不像auto模式中相同流的数据包会被分配到不同的detect线程处理。
   workers:workers模式,每个线程完成从接收到日志记录等操作。
 
● 看到这里已经对运行模式基本了解,为了进一步了解模式操作,接着深入
   存储运行模式的结构体定义如下:
   typedef struct RunMode_ {
      int runmode;              // 对应RUNMODE_PCAP_DEV等
      const char *name;         // 对应autofp等
      const char *description;  // 描述
      int (*RunModeFunc)(DetectEngineCtx *); // 处理函数
   } RunMode;
  
   typedef struct RunModes_ {
      int no_of_runmodes;
      RunMode *runmodes;
   } RunModes;
  
● 接着具体看一下RunModeRegisterNewRunMode()函数的内容
   RunModeGetCustomMode(runmode, name);// 参数对应于(RUNMODE_PCAP_DEV, "autofp")
                                      // 函数作用貌似是判断自定义模式是否已经注册
  
   // 这里意思是注册一种类型,动态添加一块内存。
   runmodes[runmode].runmodes = 
   SCRealloc(runmodes[runmode].runmodes,(runmodes[runmode].no_of_runmodes + 1) * sizeof(RunMode));
   // 指针数组用法
   RunMode *mode = &runmodes[runmode].runmodes[runmodes[runmode].no_of_runmodes];
   runmodes[runmode].no_of_runmodes++;

   // 以下是填充具体内容
   mode->runmode = runmode;
   mode->name = SCStrdup(name);
   mode->description = SCStrdup(description);
   mode->RunModeFunc = RunModeFunc;
  
● 到这里为止,已经对“模式”的概念基本了解
   suricata的运作模式是根据数据源的不同而区分,其中内部的自定义模式主要和程序架构有关,具体可以根据需求选择。
  
● 还有一些概念性的东西不明白,例如运行模式中ipfw、dag、napatech等概念,有时间再补充吧!

结合以上知识,再看http://blog.csdn.net/firedb/article/details/7581853中《Surciata源码分析之IpsNFQ模式》便很容易理解。

● 盗用链接中图片
autofp模式:


auto模式:


worker模式:

  



线程、槽和模块之间的关系

suricata中tv、slot和tm的关系必须要搞清楚,汇总如下:

tv:ThreadVars类型,线程。
slot:TmSlot类型,槽。
tm:TmModule类型,模块。

下面必须要结合三者的定义,阅读代码的时候也关注下三者关系。
----------------------------------------
线程的定义:
typedef struct ThreadVars_ {
    pthread_t t;                      // 线程id
    char *name;                       // 线程name
    char *thread_group_name;          // 线程group name
    SC_ATOMIC_DECLARE(unsigned short, flags); // 原子声明,不知道作用,暂且不管
    uint8_t aof;                      // 线程遇到故障时怎么做
    uint8_t type;                     // 线程类型,例如:TVT_PPT, TVT_MGMT
    uint8_t restarted;                // 线程重新启动失败的次数
    Tmq *inq;
    Tmq *outq;
    void *outctx;
    char *outqh_name
    struct Packet_ * (*tmqh_in)(struct ThreadVars_ *);
    void (*InShutdownHandler)(struct ThreadVars_ *);
    void (*tmqh_out)(struct ThreadVars_ *, struct Packet_ *);
    void *(*tm_func)(void *);
    struct TmSlot_ *tm_slots;
    uint8_t thread_setup_flags;
    uint16_t cpu_affinity;
    int thread_priority;                // 线程优先级
    SCPerfContext sc_perf_pctx;
    SCPerfCounterArray *sc_perf_pca;
    SCMutex *m;
    SCCondT *cond;

    uint8_t cap_flags;
    struct ThreadVars_ *next;
    struct ThreadVars_ *prev;
} ThreadVars;
-----------------------------------------
槽slot的定义:
typedef struct TmSlot_ {

    ThreadVars *tv;                       // 拥有该slot的线程
    SC_ATOMIC_DECLARE(TmSlotFunc, SlotFunc);// 函数指针
    TmEcode (*PktAcqLoop)(ThreadVars *, void *, void *);      // 模块数据包获取函数
    TmEcode (*SlotThreadInit)(ThreadVars *, void *, void **); // 模块初始化执行函数
    void (*SlotThreadExitPrintStats)(ThreadVars *, void *);   // 模块退出打印函数
    TmEcode (*SlotThreadDeinit)(ThreadVars *, void *);        // 模块清理执行函数
    void *slot_initdata;  // 数据存储
    SC_ATOMIC_DECLARE(void *, slot_data);
    PacketQueue slot_pre_pq;
    PacketQueue slot_post_pq;
    int tm_id;  // tm ID
    int id;     // slot ID
    struct TmSlot_ *slot_next;
} TmSlot;
-------------------------------------------------
模块定义:
typedef struct TmModule_ {
    char *name;          // 模块名称
    TmEcode (*ThreadInit)(ThreadVars *, void *, void **);
    void (*ThreadExitPrintStats)(ThreadVars *, void *);
    TmEcode (*ThreadDeinit)(ThreadVars *, void *);
    TmEcode (*Func)(ThreadVars *, Packet *, void *, PacketQueue *, PacketQueue *);
    TmEcode (*PktAcqLoop)(ThreadVars *, void *, void *);
    TmEcode (*Init)(void);
    TmEcode (*DeInit)(void);
    void (*RegisterTests)(void);
    uint8_t cap_flags;  
    uint8_t flags;
} TmModule;
===============================================
将三者的定义放在一起目的是方便查看,其中部分变量目前还不清楚具体含义,日后补充。
三者之间关系如下图所示:


虽然上图画的比较山寨,但应该可以清楚的说明三者之间的关系了,每一个线程都包含一个slot的链表,每个slot结点都悬挂着不同的模块,程序执行的时候会遍历slot链表,按照加入链表的熟悉执行模块。



再从main()函数看起 --- 模式、模块、线程和槽

 

经过上面几篇Blog的学习步骤,到今天再回过头去看了下main函数的执行过程,把重点几个步骤重新整理了下,方便更深入的理解架构。

1. 注册各种运行模式
   RunModeRegisterRunModes()函数
   
   RunModeIdsPcapRegister();      // IDS+pcap
   RunModeFilePcapRegister();     // File+pcap
   RunModeIdsPfringRegister();    // IDS+pfring
   RunModeIpsIPFWRegister();      // IPS+ipfw
   RunModeIpsNFQRegister();       // IPS+nfq
   RunModeErfFileRegister();      // erf+file
   RunModeErfDagRegister();       // erf+dag
   RunModeNapatechRegister();     // napatech
   RunModeIdsAFPRegister();       // IDS+AFP
   RunModeUnixSocketRegister();   // UnixSocket
   其中每一种运行模式调用RunModeRegisterNewRunMode注册各自的Custom mode(暂且翻译为“自定义模式”)
   
   RunModeRegisterNewRunMode设置各种运行模式的执行函数
   例如:RunModeRegisterNewRunMode(RUNMODE_PCAP_DEV, "single",
                              "Single threaded pcap live mode",
                              RunModeIdsPcapSingle);
   将执行函数添加到runmodes全局数组中。
   全局Runmodes类型数组runmodes保存运行模式,存储结构如下图:
   

2. 注册模块
   注册suricata所支持的所有线程模块
   
   TmModuleReceiveNFQRegister();
   TmModuleVerdictNFQRegister();
   TmModuleDecodeNFQRegister();
   
   TmModuleReceiveIPFWRegister();
   TmModuleVerdictIPFWRegister();
   TmModuleDecodeIPFWRegister();
   
   TmModuleReceivePcapRegister();
   TmModuleDecodePcapRegister();
   
   TmModuleReceivePcapFileRegister();
   TmModuleDecodePcapFileRegister();
   ……
   ……
   函数内部实现:
   void TmModuleReceivePcapRegister (void) 
   {
    tmm_modules[TMM_RECEIVEPCAP].name = "ReceivePcap";
    tmm_modules[TMM_RECEIVEPCAP].ThreadInit = ReceivePcapThreadInit;
    tmm_modules[TMM_RECEIVEPCAP].Func = NULL;
    tmm_modules[TMM_RECEIVEPCAP].PktAcqLoop = ReceivePcapLoop;
    tmm_modules[TMM_RECEIVEPCAP].ThreadExitPrintStats = ReceivePcapThreadExitStats;
    tmm_modules[TMM_RECEIVEPCAP].ThreadDeinit = NULL;
    tmm_modules[TMM_RECEIVEPCAP].RegisterTests = NULL;
    tmm_modules[TMM_RECEIVEPCAP].cap_flags = SC_CAP_NET_RAW;
    tmm_modules[TMM_RECEIVEPCAP].flags = TM_FLAG_RECEIVE_TM;
   }
   保存在全局TmModule tmm_modules[TMM_SIZE]数组中。
   typedef struct TmModule_ 
   {
      char *name;
      TmEcode (*ThreadInit)(ThreadVars *, void *, void **); // 线程初始化函数
      void (*ThreadExitPrintStats)(ThreadVars *, void *); // 线程退出打印函数
      TmEcode (*ThreadDeinit)(ThreadVars *, void *); // 线程关闭函数
      TmEcode (*Func)(ThreadVars *, Packet *, void *, PacketQueue *, PacketQueue *);
      TmEcode (*PktAcqLoop)(ThreadVars *, void *, void *);
      TmEcode (*Init)(void);// 全局初始化模块函数
      TmEcode (*DeInit)(void);// 全局关闭模块函数
      void (*RegisterTests)(void);
      uint8_t cap_flags;  
      uint8_t flags;
    } TmModule;
  
  存储结构如下图所示:
   

3. 模块初始化
   TmModuleRunInit()函数
   调用tmm_modules[TMM_SIZE]数组中模块各个模块初始化函数。
    for (i = 0; i < TMM_SIZE; i++)
    {
        t = &tmm_modules[i];
        t->Init(); // 注意这里执行的是模块全局初始化函数
    }

4. 运行模式调度
   RunModeDispatch()函数
  • 从配置中读取运行模式。
  • 获得该运行模式中默认的Custom mode(如:single、auto等)。
  • 执行Custom mode中设置的执行函数,如上图中所示的“执行函数”。

5. 运行模式执行函数
   例如:RunModeFilePcapSingle()
  • 通用模块初始化RunModeInitialize
  • 创建tv实例TmThreadCreatePacketHandler
  • 从tmm_modules中获得模块TmModuleGetByName
  • 插入槽slot
  • TmThreadSpawn真正创建线程函数


整理下执行顺序:
  • 运行模式注册,设置执行函数
  • 所有模块注册,设置模块相关函数
  • 所有模块初始化
  • 从配置获取运行模式类型,执行函数
  • 创建线程
  • 根据模块名称从全局数组tmm_modules中得到模块指针
  • 插入线程槽slot


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章