suricata源码笔记:main函数

main函数

main()函数位于suricata.c文件,其主要流程如下:

  1. 定义并初始化程序的全局实例变量。
  • SCInstance类型的suri变量用来保存程序当前的一些状态、标志等上下文环境,通常是用来作为参数传递给各个模块的子函数,因此为了更好的封装性而放到一个结构体变量中,而不是使用零散的长串参数或一堆全局变量。
  • SCInstanceInit函数,顾名思义,即是对suri中各个字段进行初始化。注意,这里对所有字段都进行了显示初始化,因为虽然一个memset清零已经基本达到目的了,但显示地将各个成员设成0/NULL/FALSE对于可读性来说还是有好处的,可以明确地说明各个字段的初始值,且对扩展性也会有好处,例如若后续初始化需要设置一些非0值(如用-1表示无效值),直接更改就好了。
  1. 初始化sc_set_caps为FALSE –> 标识是否对主线程进行特权去除(drop privilege),主要是出于安全性考虑。

  2. 初始化原子变量engine_stage –> 记录程序当前的运行阶段:SURICATA_INIT、SURICATA_RUNTIME、SURICATA_FINALIZE

  3. 初始化日志模块,因为后续的执行流程中将使用日志输出,所以需要最先初始化该模块。

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

  5. 初始化ParserSize模块 –> 使用正则表达式来解析类似“10Mb”这种大小参数,其中正则引擎用的是pcre,因此初始化时就是调用pcre_compile、pcre_study对已经写好的正则表达式进行编译和预处理。

  6. 注册各种运行模式。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),等后续初始化阶段确定了具体的运行模式后,就会调用这里注册的对应的初始化函数,对该模式下的运行环境进行进一步配置。
  1. 初始化引擎模式为IDS模式。引擎模式只有两种:IDS、IPS,初始默认为IDS,而在nfq或ipfw启用时,就会切换成IPS模式,该模式下能够执行“Drop”操作,即拦截数据包。

  2. 初始化配置模块,为配置节点树建立root节点。

  3. 解析命令行参数。其中,与包捕获相关的选项(如“-i”)都会调用LiveRegisterDevice,以注册一个数据包捕获设备接口(如eth0)。全局的所有已注册的设备接口存储在变量live_devices中,类型为LiveDevice。注意,用多设备同时捕获数据包这个特性在Suricata中目前还只是实验性的。“-v”选项可多次使用,每个v都能将当前日志等级提升一级。

  4. 若运行模式为内部模式,则进入该模式执行,完毕后退出程序。

  5. FinalizeRunMode,即为运行模式的处理划上句号。主要是设置offline标志、对unknown运行模式进行报错,以及设置全局的run_mode变量。

  6. 若运行模式为单元测试模式,则跑(用户通过正则表达式指定的)单元测试,并输出测试结果。

  7. 检查当前模式是否与daemon标志冲突。Pcap文件模式及单元测试模式都不能在daemon开启下进行。

  8. 初始化全局变量。包括:数据包队列trans_q、数据队列data_queues(干嘛的?)、对应的mutex和cond、建立小写字母表。

  9. 初始化时间。包括:获取当前时间所用的spin lock,以及设置时区(调用tzset()即可)。

  10. 快速模式匹配注册关键字。调用SupportFastPatternForSigMatchList函数,按照优先级大小插入到sm_fp_support_smlist_list链表中。

  11. 若用户未在输入参数中指定配置文件,则使用默认配置文件(/etc/suricata/suricata.yaml)。

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

  13. 再次初始化日志模块。这次,程序将会根据配置文件中日志输出配置(logging.outputs)填充SCLogInitData类型的结构体,调用SCLogInitLogModule重新初始化日志模块。

  14. 打印版本信息。这是Suricata启动开始后第一条打印信息

  15. 打印当前机器的CPU/核个数,这些信息是通过sysconf系统函数获取的。

  16. 若运行模式为DUMP_CONFIG,则调用ConfDump打印出当前的所有配置信息。ConfDump通过递归调用ConfNodeDump函数实现对配置节点树的DFS(深度优先遍历)。

\24. 执行PostConfLoadedSetup,即运行那些需要在配置载入完成后就立马执行的函数。这里面涉及的流程和函数非常多:

  • MpmTableSetup:设置多模式匹配表,该表中每一项就是一个实现了某种多模式匹配算法(如WuManber、AC)的匹配器。以注册AC匹配器为例,MpmTableSetup会调用MpmACRegister函数实现AC注册,函数内部其实只是填充mpm_table中对应AC的那一项(mpm_table[MPM_AC])的各个字段,如:匹配器名称("ac")、初始化函数(SCACInitCtx)、增加模式函数(SCACAddPatternCS)、实际的搜索执行函数(SCACSearch)。
  • 设置rule_reload标志。如果配置文件中对应选项打开,则会设置该标志,表示可以进行“规则热重载”,即能够在程序运行时载入或替换规则集。
  • AppLayerDetectProtoThreadInit:初始化应用层协议检测模块。其中,AlpProtoInit函数初始化该模块所用到的多模式匹配器,RegisterAppLayerParsers函数注册各种应用层协议的解析器(如RegisterHTPParsers函数对应HTTP协议),而AlpProtoFinalizeGlobal函数完成一些收尾工作,包括调用匹配器的预处理(Prepare)函数、建立模式ID和规则签名之间的映射等。
  • AppLayerParsersInitPostProcess:这个函数内部建立了一个解析器之间的映射,还不太懂其用途。
  • 设置并验证日志存储目录是否存在。若配置文件中未指定,则使用默认目录,linux下默认为/var/log/suricata。
  • 获取与包捕获相关的一些配置参数,目前包括:max-pending-packets、default-packet-size。
  • 设置host_mode(主机模式),两种模式:router和sniffer-only,而如果设置为“auto”,则会进行自动选择:IPS模式下运行为router,否则为sniffer-only。
  • SCHInfoLoadFromConfig:从配置文件中载入host os policy(主机OS策略)信息。网络入侵通常是针对某些特定OS的漏洞,因此如果能够获取部署环境中主机的OS信息,肯定对入侵检测大有裨益。具体这些信息是怎么使用的,暂时也还不清楚。
  • DefragInit:初始化IP分片重组模块。
  • SigTableSetup:初始化检测引擎,主要是注册检测引擎所支持的规则格式(跟Snort规则基本一致)中的关键字,比如sid、priority、msg、within、distance等等。
  • TmqhSetup:初始化queue handler(队列处理函数),这个是衔接线程模块数据包队列之间的桥梁,目前共有5类handler:simple, nfq, packetpool, flow, ringbuffer。每类handler内部都有一个InHandlerOutHandler,一个用于从上一级队列中获取数据包,另一个用于处理完毕后将数据包送入下一级队列。
  • 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端口号)是否符合格式要求。
  • RegisterAllModules:这是个非常重要的函数!里面注册了Suricata所支持的所有线程模块(Thread Module)。以pcap相关模块为例,TmModuleReceivePcapRegister函数注册了Pcap捕获模块,而TmModuleDecodePcapRegister函数注册了Pcap数据包解码模块。所谓注册,就是在tmm_modules模块数组中对应的那项中填充TmModule结构的所有字段,这些字段包括:模块名字、线程初始化函数、包处理或包获取函数、线程退出清理函数、一些标志位等等。
  • AppLayerHtpNeedFileInspection:设置suricata内部模块与libhtp(HTTP处理库)对接关系的函数,具体细节暂时不管。
  • DetectEngineRegisterAppInspectionEngines:名字都这么长了,肯定很复杂。。。暂时不管。
  • 若设置了rule_reload标志,则注册相应的信号处理函数(目前设置的函数都是些提示函数,没有做实际重载)。这里用的是比较惯用的SIGUSR2信号来触发rule reload。
  • StorageFinalize:关闭storage模块的注册,为已注册的storage实际分配存储空间。
  • TmModuleRunInit:调用之前注册的线程模块的初始化函数进行初始化。
  1. 检查是否进入Daemon模式。若需要进入Daemon模式,则会检测pidfile是否已经存在(daemon下只能有一个实例运行),然后进行Daemonize,最后创建一个pidfile。Daemonize的主要思路是:fork->子进程调用setsid创建一个新的session,关闭stdin、stdout、stderr,并告诉父进程 –> 父进程等待子进程通知,然后退出 –> 子进程继续执行。

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

  3. 获取配置文件中指定的Suricata运行时的usergroup,如果命令行中没有指定的话。然后,将指定的user和group通过getpwuid、getpwnam、getgrnam等函数转换为uid和gid,为后续的实际设置uid和gid做准备。注意,这段代码也是在InitSignalHandler中执行的,不知道为什么放这里,跟信号有关系么。。。

  4. 初始化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呢?)。

  5. 初始化Host engine。这货好像跟之前的host类型的storage有关系,具体怎么用后面再看看吧。

  6. 初始化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时间。

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

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

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

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

  11. 设置是否延迟检测。若delayed-detect为yes,则系统将在载入规则集之前就开始处理数据包,这样能够在IPS模式下将少系统的down time(宕机时间)。

  12. 如果没有设置延迟检测,就调用LoadSignatures载入规则集

  13. 如果设置了live_reload,则重新注册用于规则重载的SIGUSR2信号处理函数(这次是设置为真正的重载处理函数)。放在这里是为了防止在初次载入规则集时就被触发重载。

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

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

  16. 调用gettimeofday保存当前时间,存储在suri->start_time中,作为系统的启动时间。

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

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

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

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

  21. 初始化运行模式。首先,根据配置文件和程序中的默认值来配置运行模式(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中存储的输出模块。
  1. 若unix-command为enable状态,则创建Unix-socket命令线程,可与suricata客户端使用JSON格式信息进行通信。命令线程的创建是通过TmThreadCreateCmdThread函数,创建的线程类型为TVT_CMD。线程执行函数为UnixManagerThread

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

  3. 初始化Stream TCP模块。其中调用了StreamTcpReassembleInit函数进行重组模块初始化。

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

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

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

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

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

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

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

  11. 接着,程序就进入到了退出阶段,首先会更新engine_stage为SURICATA_DEINIT,然后依次关闭Unix-socket线程、Flow管理线程。

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

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

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

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容