0x00 决定阅读顺序

从本文开始,我们将要深入 Suricata 的代码实现。前一篇文章提到过,Suricata 的代码量大约是 sqlite3 的六倍,因此,想要阅读全部代码是不可能的,我们必须有所选择。笔者的最终目标是实现应用层的二次开发——举个例子,假设有一个私有的建立在 TCP 之上的协议,那么笔者希望能修改 Suricata,使之识别出这种协议,并将数据原文 dump 到硬盘中。于是,对笔者而言,去阅读网卡驱动相关的代码就显得不太必要了。

在学习计算机网络时,一个通行的做法是“自顶向下”,即先假设有可靠的字节流传输协议,在此基础上学习 HTTP 等应用层协议;再假设有尽力而为的数据报传输协议,在此基础上学习 TCP……依此类推,直到数据链路层。显然,我们也可以“自顶向下”地阅读 Suricata 源码,从最高层的逻辑开始,向下层逐渐探索,并在我们不再关心更底层原理的时候停止阅读。

于是,我们先阅读 alert 相关代码。我们要回答的第一个问题是:警报是如何产生的。

0x01 搭建调试环境

先进行动态分析,以便尽快定位到 alert 产生的逻辑。编译一份代码,生成 compile_commands.json 给 clangd 分析器用:

CC=clang CXX="clang++" ./configure --enable-shared=no --enable-static=yes CFLAGS="-g -O0" --disable-gccmarch-native --prefix=/home/neko/workspace/suricata-dbg/lab/app/ --sysconfdir=/home/neko/workspace/suricata-dbg/lab/conf/ --localstatedir=/home/neko/workspace/suricata-dbg/lab/state/
bear -- make -j16

# 安装到 /home/neko/workspace/suricata-dbg/lab 下
make install
make install-conf

配置 /home/neko/workspace/suricata-dbg/lab/state/lib/suricata/rules/suricata.rules

alert http any any -> any any (msg:"CVE-2023-1389 attack"; http.uri; content:"/cgi-bin/luci/\;stok=/locale?form=country"; nocase; sid:1000001; rev:1;)

准备一个最简单的用例:

# IDS 机器
sudo tcpdump -i ens224 -w one.pcap 

# host2 机器
curl --http1.0 "http://192.168.25.21:8000/cgi-bin/luci/;stok=/locale?form=country"

于是,获得一个很干净的 pcap 文件:

现在,我们能运行起来 Suricata 了:

/home/neko/workspace/suricata-dbg/lab/app/bin/suricata -r /home/neko/workspace/one.pcap

接下来,配置 vscode。由于 Suricata 会向 eve.json 尾部添加信息(而非重写),我们每次调试运行之前,需要清空工作目录。编写 .vscode/tasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "clean",
            "type": "shell",
            "command": "rm /home/neko/workspace/suricata-dbg/lab/run/*"
        }
    ]
}

编写 .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "name": "suricata replay",
      "program": "/home/neko/workspace/suricata-dbg/lab/app/bin/suricata",
      "preLaunchTask": "clean",
      "args": [
        "--runmode",
        "single",
        "-r",
        "/home/neko/workspace/one.pcap"
      ],
      "cwd": "/home/neko/workspace/suricata-dbg/lab/run/"
    },
    // ...
  ]
}

现在,按下 F5 就能跑起来程序。我们开始调试。

0x02 rules 解析逻辑

我们通过 rules 文件定义了一条 CVE-2023-1389 规则,Suricata 在启动之后需要读取 rules 文件,并 parse 规则。推测 rules 文件内容先进入一个词法分析器,然后存储到相关的数据结构中。我们修改 launch.json,让 lldb 在程序试图调用 open 函数时暂停:

      "initCommands": [
        "breakpoint set -n open"
      ]

运行程序,成功暂停:

可见 Suricata 试图打开的第一个文件是 suricata.yaml。我们把程序运行期间,打开的所有文件依次列出来:

"/home/neko/workspace/suricata-dbg/lab/conf/suricata//suricata.yaml"
"./suricata.log"
"./fast.log"
"./eve.json"
"./stats.log"
"/home/neko/workspace/suricata-dbg/lab/conf/suricata/classification.config"
"/home/neko/workspace/suricata-dbg/lab/conf/suricata/reference.config"
"/home/neko/workspace/suricata-dbg/lab/state/lib/suricata/rules/suricata.rules"
"/home/neko/workspace/suricata-dbg/lab/conf/suricata//threshold.config"
"/home/neko/workspace/one.pcap"
"/dev/urandom"

这个文件列表没有超出我们的预期。现在专注于 suricata.rules

      "initCommands": [
        "breakpoint set -n open --condition '(bool)strstr((char*)$arg1, \"suricata.rules\")'"
      ]

按下 F5 运行程序,在读取 rules 之前暂停。观察调用栈:

从 main 开始逐个分析。main 函数只有一句话:

int main(int argc, char **argv)
{
    return SuricataMain(argc, argv);
}

跟进 SuricataMain 函数,这是真正的程序入口点。

int SuricataMain(int argc, char **argv)
{
    // 初始化 suricata 这个全局变量(类型为 SCInstance),内含 log_dir, conf_filename 等属性  
    SCInstanceInit(&suricata, argv[0]);
    if (InitGlobal() != 0) {
        exit(EXIT_FAILURE);
    }
    if (ParseCommandLine(argc, argv, &suricata) != TM_ECODE_OK) {
        exit(EXIT_FAILURE);
    }

    // 检查 runmode 是否合法
    if (FinalizeRunMode(&suricata, argv) != TM_ECODE_OK) {
        exit(EXIT_FAILURE);
    }

    // 处理一些内部 runmode,例如 RUNMODE_PRINT_BUILDINFO、RUNMODE_UNITTEST
    switch (StartInternalRunMode(&suricata, argc, argv)) {
        case TM_ECODE_DONE:
            exit(EXIT_SUCCESS);
        case TM_ECODE_FAILED:
            exit(EXIT_FAILURE);
    }

    /* Initializations for global vars, queues, etc (memsets, mutex init..) */
    GlobalsInitPreConfig();

    // 读 suricata.yaml 配置文件
    /* Load yaml configuration file if provided. */
    if (LoadYamlConfig(&suricata) != TM_ECODE_OK) {
        exit(EXIT_FAILURE);
    }
    if (suricata.run_mode == RUNMODE_DUMP_CONFIG) {
        ConfDump();
        exit(EXIT_SUCCESS);
    }

    /* 一些 vlan 相关代码,略 */
    
    // Suricata 需要从 root 启动并初始化,但支持切换到非 root 用户执行
    SetupUserMode(&suricata);
    InitRunAs(&suricata);

    // 接下来是一些日志相关代码
    /* Since our config is now loaded we can finish configurating the
     * logging module. */
    SCLogLoadConfig(suricata.daemon, suricata.verbose, suricata.userid, suricata.groupid);
    LogVersion(&suricata);
    UtilCpuPrintSummary();
    
    // 初始化多线程设置
    RunModeInitializeThreadSettings();

    if (suricata.run_mode == RUNMODE_CONF_TEST)
        SCLogInfo("Running suricata under test mode");

    // 解析网卡列表
    if (ParseInterfacesList(suricata.aux_run_mode, suricata.pcap_dev) != TM_ECODE_OK) {
        exit(EXIT_FAILURE);
    }
    
    // 这里面有一些“需要在配置文件载入之后执行”的代码,例如设置 checksum_validation 是否开启
    if (PostConfLoadedSetup(&suricata) != TM_ECODE_OK) {
        exit(EXIT_FAILURE);
    }

    // 放弃一些权限
    SCDropMainThreadCaps(suricata.userid, suricata.groupid);
    
    /* Re-enable coredumps after privileges are dropped. */
    CoredumpEnable();

    if (suricata.run_mode != RUNMODE_UNIX_SOCKET && !suricata.disabled_detect) {
        suricata.unix_socket_enabled = ConfUnixSocketIsEnable();
    }

    // 一些需要在“放弃权限之后、开始 packet 处理之前”执行的初始化逻辑
    PreRunPostPrivsDropInit(suricata.run_mode);

    // 利用 landlock 配置一些权限控制策略
    LandlockSandboxing(&suricata);

    // 这个函数打开了 rules 文件,即将跟进
    PostConfLoadedDetectSetup(&suricata);
    if (suricata.run_mode == RUNMODE_ENGINE_ANALYSIS) {
        goto out;
    } else if (suricata.run_mode == RUNMODE_CONF_TEST){
        SCLogNotice("Configuration provided was successfully loaded. Exiting.");
        goto out;
    } else if (suricata.run_mode == RUNMODE_DUMP_FEATURES) {
        FeatureDump();
        goto out;
    }

读完上面的代码,我们得知 runmode 其实不止文档中列出的 workers 那三个,而是另有很多隐藏的 runmode,可以在 src/runmodes.h 查看。跟进 PostConfLoadedDetectSetup 函数:

void PostConfLoadedDetectSetup(SCInstance *suri)
{
    DetectEngineCtx *de_ctx = NULL;
    if (!suri->disabled_detect) {
        // 处理一些配置项
        SetupDelayedDetect(suri);
        int mt_enabled = 0;
        (void)ConfGetBool("multi-detect.enabled", &mt_enabled);
        int default_tenant = 0;
        if (mt_enabled)
            (void)ConfGetBool("multi-detect.default", &default_tenant);
        
        // 以下代码用于初始化 de_ctx (类型为 DetectEngineCtx)
        // DD 是 Delayed Detect 的缩写,MT 是 Multi Tenant 的缩写
        if (DetectEngineMultiTenantSetup(suri->unix_socket_enabled) == -1) {
            FatalError("initializing multi-detect "
                       "detection engine contexts failed.");
        }
        if (suri->delayed_detect && suri->run_mode != RUNMODE_CONF_TEST) {
            de_ctx = DetectEngineCtxInitStubForDD();
        } else if (mt_enabled && !default_tenant && suri->run_mode != RUNMODE_CONF_TEST) {
            de_ctx = DetectEngineCtxInitStubForMT();
        } else {
            de_ctx = DetectEngineCtxInit();
        }
        if (de_ctx == NULL) {
            FatalError("initializing detection engine failed.");
        }

        if (de_ctx->type == DETECT_ENGINE_TYPE_NORMAL) {
            // 载入 rules,待跟进
            if (LoadSignatures(de_ctx, suri) != TM_ECODE_OK)
                exit(EXIT_FAILURE);
        }

        gettimeofday(&de_ctx->last_reload, NULL);
        DetectEngineAddToMaster(de_ctx);
        DetectEngineBumpVersion();
    }
}
💡
上面的代码中出现了 delayed detect 概念。根据文章,delayed detect 模式下,suricata 可以在完全载入规则列表之前就开始处理报文。
💡
multi tenant 允许不同的 tenant 使用各自的规则。可以把 vlan 或网卡等绑定到 tenant,详见文档

可见,在常规 detect engine 模式下,此函数会调用 LoadSignatures(de_ctx, suri) 载入规则文件。跟进:

static int LoadSignatures(DetectEngineCtx *de_ctx, SCInstance *suri)
{
    if (SigLoadSignatures(de_ctx, suri->sig_file, suri->sig_file_exclusive) < 0) {
        SCLogError("Loading signatures failed.");
        if (de_ctx->failure_fatal)
            return TM_ECODE_FAILED;
    }

    return TM_ECODE_OK;
}

继续跟进 SigLoadSignatures()

int SigLoadSignatures(DetectEngineCtx *de_ctx, char *sig_file, int sig_file_exclusive)
{
    /* 变量声明,略 */

    // 拼接文件名。本次运行中 config_prefix 为空串,varname = "rule-files"
    if (strlen(de_ctx->config_prefix) > 0) {
        snprintf(varname, sizeof(varname), "%s.rule-files",
                de_ctx->config_prefix);
    }

    if (RunmodeGetCurrent() == RUNMODE_ENGINE_ANALYSIS) {
        SetupEngineAnalysis(de_ctx, &fp_engine_analysis_set, &rule_engine_analysis_set);
    }

    /* ok, let's load signature files from the general config */
    if (!(sig_file != NULL && sig_file_exclusive == TRUE)) {
        rule_files = ConfGetNode(varname);
        // ConfNode 组成了一个树形结构,采用“兄弟-儿子”存储方法,每个节点有 char* 的 name 和 val
        // 每个节点的 *parent 指向父节点,*head 指向第一个子节点,*next 指向同层的后一个节点
        // ConfGetNode 函数的作用是在树形结构中查找 name 为特定值的结点
        
        if (rule_files != NULL) {
            if (!ConfNodeIsSequence(rule_files)) {
                SCLogWarning("Invalid rule-files configuration section: "
                             "expected a list of filenames.");
            }
            else {
                // 在本次运行中,ConfGetNode("rule-files") 获取到了一个 name="rule-files" val=NULL 的节点
                // 从该节点的 head 开始,我们可以遍历到配置文件,不过仅有一项:name="0" val="suricata.rules"
                TAILQ_FOREACH(file, &rule_files->head, next) {
                    // 拼接出完整 rules 文件路径 
                    sfile = DetectLoadCompleteSigPath(de_ctx, file->val);
                    good_sigs = bad_sigs = skipped_sigs = 0;
                    
                    // 处理 rules 文件,待跟进
                    ret = ProcessSigFiles(
                            de_ctx, sfile, sig_stat, &good_sigs, &bad_sigs, &skipped_sigs);
                    SCFree(sfile);

                    if (de_ctx->failure_fatal && ret != 0) {
                        /* Some rules failed to load, just exit as
                         * errors would have already been logged. */
                        exit(EXIT_FAILURE);
                    }

                    if (good_sigs == 0) {
                        SCLogConfig("No rules loaded from %s.", file->val);
                    }
                }
            }
        }
    }

    // 载入命令行参数提供的 rules 文件
    /* If a Signature file is specified from command-line, parse it too */
    if (sig_file != NULL) {
        ret = ProcessSigFiles(de_ctx, sig_file, sig_stat, &good_sigs, &bad_sigs, &skipped_sigs);

        /* 错误处理同上,略 */
    }

    /* 打一些日志,告诉用户载入了多少条规则,略 */
    
    // 注册一些 signature 排序函数,例如 SCSigOrderByActionCompare、SCSigOrderByFlowbitsCompare
    SCSigRegisterSignatureOrderingFuncs(de_ctx);
    
    // 执行 signature 排序
    SCSigOrderSignatures(de_ctx);
    SCSigSignatureOrderingModuleCleanup(de_ctx);

    if (SCThresholdConfInitContext(de_ctx) < 0) {
        ret = -1;
        goto end;
    }

    /* Setup the signature group lookup structure and pattern matchers */
    if (SigGroupBuild(de_ctx) < 0)
        goto end;

    ret = 0;

 end:
    /* 释放空间,略 */
    
    SCReturnInt(ret);
}
💡
ConfNode 的根节点是 src/conf.c 中定义的静态全局变量 *root。目前,它拥有的子节点包括:pcap-filesuricata-versionvarsdefault-log-dir 、stats 、plugins 、outputs 、logging 等。 

读完之后,可见该函数的作用就是从文件中载入规则(代码中常常称为 signature),然后对规则进行排序。跟进 ProcessSigFiles() 函数:

// 简化后的代码
static int ProcessSigFiles(DetectEngineCtx *de_ctx, char *pattern, SigFileLoaderStat *st,
        int *good_sigs, int *bad_sigs, int *skipped_sigs)
{
    int r = 0;

    if (pattern == NULL) {
        SCLogError("opening rule file null");
        return -1;
    }

    char *fname = pattern;
    if (strcmp("/dev/null", fname) == 0)
        return 0;

    // 载入规则文件
    SCLogConfig("Loading rule file: %s", fname);
    r = DetectLoadSigFile(de_ctx, fname, good_sigs, bad_sigs, skipped_sigs);
    if (r < 0) {
        ++(st->bad_files);
    }

    ++(st->total_files);

    st->good_sigs_total += *good_sigs;
    st->bad_sigs_total += *bad_sigs;
    st->skipped_sigs_total += *skipped_sigs;

    return r;
}

跟进 DetectLoadSigFile() 函数:

static int DetectLoadSigFile(
        DetectEngineCtx *de_ctx, char *sig_file, int *goodsigs, int *badsigs, int *skippedsigs)
{
    /* 变量初始化,略 */

    // 打开规则文件,我们的调试器即是在此暂停
    FILE *fp = fopen(sig_file, "r");
    if (fp == NULL) {
        SCLogError("opening rule file %s:"
                   " %s.",
                sig_file, strerror(errno));
        return -1;
    }

    // 每行最多 8192 个字节
    while(fgets(line + offset, (int)sizeof(line) - offset, fp) != NULL) {
        lineno++;
        size_t len = strlen(line);

        /* 忽略空行,略 */
        
        /* 允许用 `\` 分行,逻辑上视为一行。语法与 bash 用 `\` 分行类似。略 */

        de_ctx->rule_file = sig_file;
        de_ctx->rule_line = lineno - multiline;

        // 把这一行加入 de_ctx->sig_list,待跟进
        sig = DetectEngineAppendSig(de_ctx, line);
        if (sig != NULL) {
            if (rule_engine_analysis_set || fp_engine_analysis_set) {
                RetrieveFPForSig(de_ctx, sig);
                if (fp_engine_analysis_set) {
                    EngineAnalysisFP(de_ctx, sig, line);
                }
                if (rule_engine_analysis_set) {
                    EngineAnalysisRules(de_ctx, sig, line);
                }
            }
            SCLogDebug("signature %"PRIu32" loaded", sig->id);
            good++;
        } else {
            /* 错误处理,略 */
        }
        multiline = 0;
    }
    
    /* 收尾工作,略 */
}

可见 DetectLoadSigFile() 函数只是一个初级的词法分析器,把规则文件按行拆分(允许用 \ 换行),对每一行规则调用 DetectEngineAppendSig()。跟进之:

 Signature *DetectEngineAppendSig(DetectEngineCtx *de_ctx, const char *sigstr)
{
    // 解析这一行规则
    Signature *sig = SigInit(de_ctx, sigstr);
    if (sig == NULL) {
        return NULL;
    }

    // 判重,利用哈希表加速
    int dup_sig = DetectEngineSignatureIsDuplicate(de_ctx, sig);
    if (dup_sig == 1) {
        // 若完全重复,或本条规则的 rev 比已有的重复规则的 rev 还旧,则报错
        SCLogError("Duplicate signature \"%s\"", sigstr);
        goto error;
    } else if (dup_sig == 2) {
        // 若重复,但此规则的版本新一些,则产生警告
        SCLogWarning("Signature with newer revision,"
                     " so the older sig replaced by this new signature \"%s\"",
                sigstr);
    }

    /* 把 sig 插入 de_ctx->sig_list 链表,略 */

    return (dup_sig == 0 || dup_sig == 2) ? sig : NULL;

error:
    /* 错误处理,略 */
}

SigInit() 函数接收一行字符串,返回一个 Signature 对象。它的实现如下:

Signature *SigInit(DetectEngineCtx *de_ctx, const char *sigstr)
{
    SCEnter();

    uint32_t oldsignum = de_ctx->signum;
    de_ctx->sigerror_ok = false;
    de_ctx->sigerror_silent = false;
    de_ctx->sigerror_requires = false;

    Signature *sig;

    if ((sig = SigInitHelper(de_ctx, sigstr, SIG_DIREC_NORMAL)) == NULL) {
        goto error;
    }

    // 若这条规则是双向的(即使用了 `<>` 而不是 `->`),且 src 与 dst 相同,则视为单向
    if (sig->init_data->init_flags & SIG_FLAG_INIT_BIDIREC) {
        if (SigHasSameSourceAndDestination(sig)) {
            SCLogInfo("Rule with ID %u is bidirectional, but source and destination are the same, "
                "treating the rule as unidirectional", sig->id);

            sig->init_data->init_flags &= ~SIG_FLAG_INIT_BIDIREC;
        } else {
            sig->next = SigInitHelper(de_ctx, sigstr, SIG_DIREC_SWITCHED);
            if (sig->next == NULL) {
                goto error;
            }
        }
    }

    SCReturnPtr(sig, "Signature");   // 展开成 return sig

error:
    /* 错误处理,略 */
}

继续跟进 SigInitHelper() 函数:

static Signature *SigInitHelper(DetectEngineCtx *de_ctx, const char *sigstr,
                                uint8_t dir)
{
    SignatureParser parser;
    memset(&parser, 0x00, sizeof(parser));
    
    // 注:为阅读方便,下面代码中省略了一些错误处理逻辑
    
    Signature *sig = SigAlloc();
    sig->sig_str = SCStrdup(sigstr);
    sig->gid = 1;

    // 第一次调用 SigParse(),requires 参数为 true,这次只判断是否存在依赖问题
    int ret = SigParse(de_ctx, sig, sigstr, dir, &parser, true);
    if (ret == -4) {
        /* Rule requirements not met. */
        de_ctx->sigerror_silent = true;
        de_ctx->sigerror_ok = true;
        de_ctx->sigerror_requires = true;
        goto error;
    } else if (ret < 0) {
        goto error;
    }
    
    // 要求规则拥有 sid 字段
    if (sig->id == 0) {
        SCLogError("Signature missing required value \"sid\".");
        goto error;
    }

    // 第二次调用 SigParse(),这次 requires 参数为 false
    ret = SigParse(de_ctx, sig, sigstr, dir, &parser, false);
    /* 错误处理已省略 */

    // 若 priority 字段未指定,则设为默认值(3)
    if (sig->prio == -1)
        sig->prio = DETECT_DEFAULT_PRIO;

    // 更新 de_ctx 记录的规则数量
    sig->num = de_ctx->signum;
    de_ctx->signum++;

    if (sig->alproto != ALPROTO_UNKNOWN) {
        // alproto 是 application layer proto 的缩写,例如 http, ftp
        // 想要看懂下面的代码,需要先知道 `sig->proto.proto` 的意义,我们暂且跳过
        int override_needed = 0;
        if (sig->proto.flags & DETECT_PROTO_ANY) {
            sig->proto.flags &= ~DETECT_PROTO_ANY;
            memset(sig->proto.proto, 0x00, sizeof(sig->proto.proto));
            override_needed = 1;
        } else {
            override_needed = 1;
            size_t s = 0;
            for (s = 0; s < sizeof(sig->proto.proto); s++) {
                if (sig->proto.proto[s] != 0x00) {
                    override_needed = 0;
                    break;
                }
            }
        }

        if (override_needed)
            AppLayerProtoDetectSupportedIpprotos(sig->alproto, sig->proto.proto);
    }

    /* 如果规则是针对 packet 而非 app layer 的,则设置一些 init_data,略 */
    
    SCLogDebug("sig %"PRIu32" SIG_FLAG_APPLAYER: %s, SIG_FLAG_PACKET: %s",
        sig->id, sig->flags & SIG_FLAG_APPLAYER ? "set" : "not set",
        sig->init_data->init_flags & SIG_FLAG_INIT_PACKET ? "set" : "not set");

    SigBuildAddressMatchArray(sig);

    // 对于每个 sigmatch 的 buffer 进行初始化
    for (uint32_t x = 0; x < DETECT_SM_LIST_MAX; x++) {
        if (sig->init_data->smlists[x])
            DetectEngineBufferRunSetupCallback(de_ctx, x, sig);
    }
    for (uint32_t x = 0; x < sig->init_data->buffer_index; x++) {
        DetectEngineBufferRunSetupCallback(de_ctx, sig->init_data->buffers[x].id, sig);
    }

    // 规则合法性验证,例如不能对 HTTP2 协议匹配文件名
    if (SigValidate(de_ctx, sig) == 0) {
        goto error;
    }

    // 设置 SignatureType,例如 SIG_TYPE_IPONLY,SIG_TYPE_STREAM,SIG_TYPE_APPLAYER
    SignatureSetType(de_ctx, sig);

    /* 错误处理,略 */
}

上面的代码调用了两次 SigParse(),其中一次只考虑 require 是否满足,另一次是真正的 parse。在 parse 完毕之后,如果规则是应用层规则,则可能会改写 sig->proto.proto,这段代码需要先知道 proto.proto 是什么,才能解释。所以我们先看 SigParse() 的实现。SigParse() 逻辑如下:

static int SigParse(DetectEngineCtx *de_ctx, Signature *s, const char *sigstr,
        uint8_t addrs_direction, SignatureParser *parser, bool requires)
{
    /* 要求规则字符串为 utf8 串。略 */

    // SigParseBasics() 只提取 action、protocol、src、sp(即 src ports 的缩写)、direction、dst、dp 字符串
    // 另外,会更新 signature 结构体
    int ret = SigParseBasics(de_ctx, s, sigstr, parser, addrs_direction, requires);
    if (ret < 0) {
        SCLogDebug("SigParseBasics failed");
        SCReturnInt(-1);
    }

    // 现在,parser->direction = "->",parser->sp = "any",parser->opts 为 msg:"CVE-2023....
    // 接下来扫描 opts 中的各种设置
    if (strlen(parser->opts) > 0) {
        size_t buffer_size = strlen(parser->opts) + 1;
        char input[buffer_size];
        char output[buffer_size];
        memset(input, 0x00, buffer_size);
        memcpy(input, parser->opts, strlen(parser->opts) + 1);

        // 每次从 opts 字符串中取出一个配置项
        do {
            memset(output, 0x00, buffer_size);
            
            // 里面是大量分类讨论
            ret = SigParseOptions(de_ctx, s, input, output, buffer_size, requires);
            if (ret == 1) {
                memcpy(input, output, buffer_size);
            }

        } while (ret == 1);

        if (ret < 0) {
            /* Suricata didn't meet the rule requirements, skip. */
            goto end;
        }
    }

end:
    DetectIPProtoRemoveAllSMs(de_ctx, s);

    SCReturnInt(ret);
}

一条规则最核心的属性,如 app layer proto 等,是 SigParseBasics() 函数设置的。因此我们跟进 SigParseBasics() 看看:

// 以下代码已简化
static int SigParseBasics(DetectEngineCtx *de_ctx, Signature *s, const char *sigstr,
        SignatureParser *parser, uint8_t addrs_direction, bool scan_only)
{
    char *index, dup[DETECT_MAX_RULE_SIZE];

    strlcpy(dup, sigstr, DETECT_MAX_RULE_SIZE);
    index = dup;

    // 规则字符串的最开始,是一些由空格隔开的属性,将它们提取出来,放进 parser 的成员变量
    // 被提取的字段依次是:action, protocol, src, sp, direction, dst, dp
    SigParseToken(&index, parser->action, sizeof(parser->action));
    SigParseList(&index, parser->protocol, sizeof(parser->protocol));
    SigParseList(&index, parser->src, sizeof(parser->src));
    SigParseList(&index, parser->sp, sizeof(parser->sp));
    SigParseToken(&index, parser->direction, sizeof(parser->direction));
    SigParseList(&index, parser->dst, sizeof(parser->dst));
    SigParseList(&index, parser->dp, sizeof(parser->dp));

    /* 提取剩余的 options,略 */

    if (scan_only) {
        return 0;
    }

    // 填写 s->action,如 alert、drop、pass、config 等。清单定义在 src/detect-parse.c
    if (SigParseAction(s, parser->action) < 0)
        goto error;

    // 解析 protocol,待跟进
    if (SigParseProto(s, parser->protocol) < 0)
        goto error;

    // 解析 direction
    if (strcmp(parser->direction, "<>") == 0) {
        s->init_data->init_flags |= SIG_FLAG_INIT_BIDIREC;
    } else if (strcmp(parser->direction, "->") != 0) {
        SCLogError("\"%s\" is not a valid direction modifier, "
                   "\"->\" and \"<>\" are supported.",
                parser->direction);
        goto error;
    }

    // 解析 IP 和端口
    if (SigParseAddress(de_ctx, s, parser->src, SIG_DIREC_SRC ^ addrs_direction) < 0)
       goto error;
    if (SigParseAddress(de_ctx, s, parser->dst, SIG_DIREC_DST ^ addrs_direction) < 0)
        goto error;
    if (SigParsePort(de_ctx, s, parser->sp, SIG_DIREC_SRC ^ addrs_direction) < 0)
        goto error;
    if (SigParsePort(de_ctx, s, parser->dp, SIG_DIREC_DST ^ addrs_direction) < 0)
        goto error;

    return 0;

error:
    return -1;
}

我们接下来只需要看 SigParseProto() 的实现,不过在那之前,我们先观察一下 Signature 的结构:

  • 每条规则对应一个 Signature。含有的属性包括: msgalprotoactionaddr_dst_match4_cnt 等。
  • 它有一个成员 SignatureInitData *init_data,而 init_data 内有指针数组 SigMatch_ *smlists[DETECT_SM_LIST_MAX] ,代码中 sm 一般是 SigMatch 的缩写。注释称 smlists 存放着 builtin 的 SigMatch 列表。文件 src/detect.h 里定义了 DETECT_SM_LIST_MATCH, DETECT_SM_LIST_BASE64_DATA 等 SigMatch 列表。
    SigMatch 列表是用链表实现的,每个 SigMatch 对象都有 prev, next 指针。SigMatch 列表可能会插入新的 SigMatch,可以参考 urilen 的例子。在这个例子中,一个新的 SigMatch 被插入进了 s->init_data->smlists
    总结一句:一个 Signature 里面有一个 init_data,而 init_data 里面有 7 个 SigMatch 列表,每个列表在初始情况下都是空的。
  • 它有一个成员 DetectProto proto ,由 256 bit(32 字节)的 proto 属性和 1 字节的 flags 属性组成。

现在来看 proto 相关的逻辑。观察 SigParseProto() 函数:

static int SigParseProto(Signature *s, const char *protostr)
{
    SCEnter();

    // 这个函数判断 proto 字符串是否为“非应用层协议”,例如 tcp、ipv6 等
    int r = DetectProtoParse(&s->proto, (char *)protostr);
    if (r < 0) {
        s->alproto = AppLayerGetProtoByName((char *)protostr);
        
        if (s->alproto != ALPROTO_UNKNOWN) {
            // 若是应用层协议,则相应配置 proto
            s->flags |= SIG_FLAG_APPLAYER;

            AppLayerProtoDetectSupportedIpprotos(s->alproto, s->proto.proto);
        }
        else {
            /* 错误处理,略 */
        }
    }

    /* if any of these flags are set they are set in a mutually exclusive
     * manner */
    if (s->proto.flags & DETECT_PROTO_ONLY_PKT) {
        s->flags |= SIG_FLAG_REQUIRE_PACKET;
    } else if (s->proto.flags & DETECT_PROTO_ONLY_STREAM) {
        s->flags |= SIG_FLAG_REQUIRE_STREAM;
    }

    SCReturnInt(0);
}

上面的代码中再次出现了 AppLayerProtoDetectSupportedIpprotos() 函数,即当时被我们跳过的代码。我们可以推测,这个函数的意图是“根据应用层协议的性质,填写底层协议的性质”。那么,我们先跟进 protostr 为底层协议的情况,看看底层协议是如何填写那个 256 bit 的 proto 的:

int DetectProtoParse(DetectProto *dp, const char *str)
{
    if (strcasecmp(str, "tcp") == 0) {
        // 把 dp->proto 的 bit 6 置为 1
        dp->proto[IPPROTO_TCP / 8] |= 1 << (IPPROTO_TCP % 8);
        SCLogDebug("TCP protocol detected");
    } else if (strcasecmp(str, "tcp-pkt") == 0) {
        // 把 dp->proto 的 bit 6 置为 1,并且标记 ONLY_PKT
        dp->proto[IPPROTO_TCP / 8] |= 1 << (IPPROTO_TCP % 8);
        SCLogDebug("TCP protocol detected, packets only");
        dp->flags |= DETECT_PROTO_ONLY_PKT;
    } else if (strcasecmp(str, "tcp-stream") == 0) {
        // 把 dp->proto 的 bit 6 置为 1,并且标记 ONLY_STREAM
        dp->proto[IPPROTO_TCP / 8] |= 1 << (IPPROTO_TCP % 8);
        SCLogDebug("TCP protocol detected, stream only");
        dp->flags |= DETECT_PROTO_ONLY_STREAM;
    } else if (strcasecmp(str, "udp") == 0) {
        // 把 dp->proto 的 bit 17 置为 1
        dp->proto[IPPROTO_UDP / 8] |= 1 << (IPPROTO_UDP % 8);
        SCLogDebug("UDP protocol detected");
    } else if (strcasecmp(str, "icmpv4") == 0) {
        // bit 1
        dp->proto[IPPROTO_ICMP / 8] |= 1 << (IPPROTO_ICMP % 8);
        SCLogDebug("ICMPv4 protocol detected");
    } else if (strcasecmp(str, "icmpv6") == 0) {
        // bit 58
        dp->proto[IPPROTO_ICMPV6 / 8] |= 1 << (IPPROTO_ICMPV6 % 8);
        SCLogDebug("ICMPv6 protocol detected");
    } else if (strcasecmp(str, "icmp") == 0) {
        // bit 1 以及 bit 58
        dp->proto[IPPROTO_ICMP / 8] |= 1 << (IPPROTO_ICMP % 8);
        dp->proto[IPPROTO_ICMPV6 / 8] |= 1 << (IPPROTO_ICMPV6 % 8);
        SCLogDebug("ICMP protocol detected, sig applies both to ICMPv4 and ICMPv6");
    } else if (strcasecmp(str, "sctp") == 0) {
        // bit 132
        dp->proto[IPPROTO_SCTP / 8] |= 1 << (IPPROTO_SCTP % 8);
        SCLogDebug("SCTP protocol detected");
    } else if (strcasecmp(str,"ipv4") == 0 ||
               strcasecmp(str,"ip4") == 0 ) {
        // 以下是 DETECT_PROTO_ANY/IPV4/IPV6 相关
        dp->flags |= (DETECT_PROTO_IPV4 | DETECT_PROTO_ANY);
        memset(dp->proto, 0xff, sizeof(dp->proto));
        SCLogDebug("IPv4 protocol detected");
    } else if (strcasecmp(str,"ipv6") == 0 ||
               strcasecmp(str,"ip6") == 0 ) {
        dp->flags |= (DETECT_PROTO_IPV6 | DETECT_PROTO_ANY);
        memset(dp->proto, 0xff, sizeof(dp->proto));
        SCLogDebug("IPv6 protocol detected");
    } else if (strcasecmp(str,"ip") == 0 ||
               strcasecmp(str,"pkthdr") == 0) {
        /* Proto "ip" is treated as an "any" */
        dp->flags |= DETECT_PROTO_ANY;
        memset(dp->proto, 0xff, sizeof(dp->proto));
        SCLogDebug("IP protocol detected");
    } else {
        goto error;
    }

    return 0;
error:
    return -1;
}

代码中的 IPPROTO_UDP 等常量定义在 /usr/include/netinet/in.h 中,对应 IP 报文中的“协议”那一个字节。因此,我们确定 s->proto.proto 就是一个用于标记底层协议的 bitmap,例如,如果这条规则是针对 udp 协议的,则 s->proto.proto 的第 17 个 bit 为 true。回头看应用层协议的处理:

    if (r < 0) {
        s->alproto = AppLayerGetProtoByName((char *)protostr);
        
        if (s->alproto != ALPROTO_UNKNOWN) {
            // 若是应用层协议,则相应配置 proto
            s->flags |= SIG_FLAG_APPLAYER;

            AppLayerProtoDetectSupportedIpprotos(s->alproto, s->proto.proto);
        }
        else {
            /* 错误处理,略 */
        }
    }

这里 AppLayerGetProtoByName() 函数就是查表获取各个应用层协议对应的常量。

💡
应用层协议清单定义在 src/app-layer-protos.h 中,例如 ALPROTO_SSH = 5ALPROTO_DNS = 11。特殊地,HTTP 协议占了三个位置: ALPROTO_HTTP1 = 1ALPROTO_HTTP2 = 31 ,以及 ALPROTO_HTTP = 33,代表“HTTP1 或 HTTP2”。

跟进 AppLayerProtoDetectSupportedIpprotos() 函数:

void AppLayerProtoDetectSupportedIpprotos(AppProto alproto, uint8_t *ipprotos)
{
    SCEnter();

    // Custom case for only signature-only protocol so far
    if (alproto == ALPROTO_HTTP) {
        AppLayerProtoDetectSupportedIpprotos(ALPROTO_HTTP1, ipprotos);
        AppLayerProtoDetectSupportedIpprotos(ALPROTO_HTTP2, ipprotos);
    } else {
        AppLayerProtoDetectPMGetIpprotos(alproto, ipprotos);
        AppLayerProtoDetectPPGetIpprotos(alproto, ipprotos);
        AppLayerProtoDetectPEGetIpprotos(alproto, ipprotos);
    }

    SCReturn;
}

前文猜测这个函数的功能是利用应用层协议的性质去填写 proto 这个 bitmap,现在我们详细看看是如何实现的。首先,对于 HTTP 协议,会把 HTTP1 和 HTTP2 的情况各执行一次;其他情况下,分别执行 PM, PP, PE 这三个函数。经过一番周折,我们确定:

  • PM 是 ProtoMapping 的缩写,把 FLOW_PROTO_TCP 映射到 IPPROTO_TCP 等。
  • PP 是 ProbingParser 的缩写,详情见这篇博客
  • PE 是 ExpectationProto 的缩写,它维护了一张 expectation_proto[ALPROTO_MAX] 表,查表可知某协议是 TCP 还是 UDP。例如,17 号应用层协议( ALPROTO_FTPDATA )在表中的对应值是 0x06,所以协议是 TCP。

由此,回顾当时被我们跳过的 SigInitHelper() 代码片段:

    if (sig->alproto != ALPROTO_UNKNOWN) {
        // alproto 是 application layer proto 的缩写,例如 http, ftp
        // 想要看懂下面的代码,需要先知道 `sig->proto.proto` 的意义,我们暂且跳过
        int override_needed = 0;
        if (sig->proto.flags & DETECT_PROTO_ANY) {
            sig->proto.flags &= ~DETECT_PROTO_ANY;
            memset(sig->proto.proto, 0x00, sizeof(sig->proto.proto));
            override_needed = 1;
        } else {
            override_needed = 1;
            size_t s = 0;
            for (s = 0; s < sizeof(sig->proto.proto); s++) {
                if (sig->proto.proto[s] != 0x00) {
                    override_needed = 0;
                    break;
                }
            }
        }

        if (override_needed)
            AppLayerProtoDetectSupportedIpprotos(sig->alproto, sig->proto.proto);
    }

显然,这一小段代码的意思是:如果有 DETECT_PROTO_ANY 标记,或者虽然没有 DETECT_PROTO_ANY 标记,但 proto.proto 全空,则我们需要利用 app layer 协议的知识,给 signature 标记它的底层协议。这是通过 AppLayerProtoDetectSupportedIpprotos() 实现的。

0x03 总结

本文的结论如下:

  • 程序的入口点是 SuricataMain 函数,它首先做一些初始化操作,从 root 切入其他用户,放弃一些权限,然后读入配置文件。
  • 配置节点形成树形结构,存法是兄弟-儿子存储方法。
  • 每个配置文件按行读入(允许用 \ 换行),每行配置文件进入 parser 处理。
  • parser 首先读取用空格隔开的基础字段,然后处理其余字段。
  • parse 过程结束后,signature 里面会记录 proto 属性,其中有 256 bit 的 bitmap,表示本规则要匹配的底层协议。
  • signature 内有 7 个内置的 SigMatch 列表,以及它们对应的 buffer。

以上,我们讨论完了“一条规则是如何载入的”。