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();
}
}
可见,在常规 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-file
、suricata-version
、vars
、default-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
。含有的属性包括:msg
、alproto
、action
、addr_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 = 5
、 ALPROTO_DNS = 11
。特殊地,HTTP 协议占了三个位置: ALPROTO_HTTP1 = 1
、 ALPROTO_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。
以上,我们讨论完了“一条规则是如何载入的”。