初始化现有的所有的一个解析器模块
int
detect_parser_init(void)
{
int rc = 0;
// 初始化红黑树存储结构
RB_INIT(&detect_parsers);
// 顺序加载内置解析器模块
TRYLOAD(rc, detect_parser_sqli);
TRYLOAD(rc, detect_parser_pt);
TRYLOAD(rc, detect_parser_bash);
done:
// 任一模块加载失败时执行全局清理
if (rc) {
detect_parser_deinit();
}
return (rc);
}
例如:detect_parser_sqli
每个模块都是detect_parser 结构体
struct detect_parser detect_parser_sqli = {
.name = {CSTR_LEN("sqli")},
.open = detect_sqli_open,
.close = detect_sqli_close,
.start = detect_sqli_start,
.stop = detect_sqli_stop,
.add_data = detect_sqli_add_data,
};
// 模块的结构体
// 每个对象都是一个函数指针。这样就可以方便的使用入口函数调用模块内的函数
struct detect_parser {
struct detect_str name;
detect_parser_init_func init;
detect_parser_deinit_func deinit;
detect_parser_open_func open;
detect_parser_close_func close;
detect_parser_set_options_func set_options;
detect_parser_start_func start;
detect_parser_stop_func stop;
detect_parser_add_data_func add_data;
};
这里调用的是SQL 那么调用就是sql的detect_parser_open_func 函数指针 最终调用到detect_sqli_open
static struct detect *
// 创建并初始化SQL注入检测器实例
detect_sqli_open(struct detect_parser *parser)
{
// 初始化一个detect 结构体
struct detect *detect;
unsigned i;
//
detect = malloc(sizeof(*detect));
//初始化检测器基础结构,设置解析器指针
detect_instance_init(detect, parser);
// 设置上下文数量(对应枚举SQLI_CTX_LAST的值)
detect->nctx = SQLI_CTX_LAST;
// 为所有上下文分配内存
detect->ctxs = malloc(detect->nctx * sizeof(*detect->ctxs)); // 申请了6块 detect_ctx 指针内存的地址
for (i = 0; i < detect->nctx; i++) {
// 每个上下文地址都是detect_ctx 这个结构体指针
// detect_ctx 结构体包含了 detect_ctx_desc detect_ctx_result 这两个结构体
struct sqli_detect_ctx *ctx;
ctx = calloc(1, sizeof(*ctx));
ctx->base.desc = (struct detect_ctx_desc *)&sqli_ctxs[i].desc; // 这里例如第一个就是data
ctx->base.res = &ctx->res;
detect_ctx_result_init(ctx->base.res); // 初始化检测结果结构体
ctx->type = i;
ctx->ctxnum = i;
ctx->detect = detect; // 指向detect 结构体
ctx->var_start_with_num = sqli_ctxs[i].var_start_with_num; // 是否变量以数字开头
// 这里存储的是sqli_detect_ctx 结构体。
// 因为sqli_detect_ctx 中第一个成员就是detect_ctx 结构体 所以等价于detect_ctx
detect->ctxs[i] = (void *)ctx;
}
// 返回初始化好的检测器
return (detect);
}
内置了5种检测类型
static const struct {
struct detect_ctx_desc desc;
enum sqli_parser_tokentype start_tok;
bool var_start_with_num;
} sqli_ctxs[] = {
// clang-format off
[SQLI_CTX_DATA] = {
.desc = {.name = {CSTR_LEN("data")}},
.start_tok = TOK_START_DATA, //上下文的开始
.var_start_with_num = false,
},
[SQLI_CTX_IN_STRING] = {
.desc = {.name = {CSTR_LEN("str")}},
.start_tok = TOK_START_STRING, //表示SQL字符串上下文的开始
.var_start_with_num = false,
},
[SQLI_CTX_RCE] = {
.desc = {.name = {CSTR_LEN("rce")}, .rce = true},
.start_tok = TOK_START_RCE, //表示远程命令执行上下文的开始
.var_start_with_num = false,
},
[SQLI_CTX_DATA_VAR_START_WITH_NUM] = {
.desc = {.name = {CSTR_LEN("data_num")}},
.start_tok = TOK_START_DATA, //数据上下文的起始令牌
.var_start_with_num = true,
},
[SQLI_CTX_IN_STRING_VAR_START_WITH_NUM] = {
.desc = {.name = {CSTR_LEN("str_num")}},
.start_tok = TOK_START_STRING, //字符串上下文的起始令牌
.var_start_with_num = true,
},
[SQLI_CTX_RCE_VAR_START_WITH_NUM] = {
.desc = {.name = {CSTR_LEN("rce_num")}, .rce = true},
.start_tok = TOK_START_RCE, //远程命令执行上下文的起始令牌
.var_start_with_num = true,
},
};
detect_sqli_start 函数都会给每个上下文进行初始化。给这个上下文最开始的一个状态。
这里使用了detect_sqli_push_token 进行写入状态
static int
detect_sqli_start(struct detect *detect)
{
unsigned i;
// 遍历所有上下文
for (i = 0; i < detect->nctx; i++) {
struct sqli_detect_ctx *ctx = (void *)detect->ctxs[i];
// 如果当前上下文已经完成,则跳过
if (ctx->res.finished){
printf("ctx %u finished\n", i);
continue;
}
// yypstate_new
ctx->pstate = sqli_parser_pstate_new();
sqli_lexer_init(&ctx->lexer);
if (detect_sqli_push_token(ctx, sqli_ctxs[ctx->type].start_tok, NULL) != 0)
break;
}
return (0);
}
re2c 代码如下
https://github.com/wallarm/libdetection/blob/master/lib/sqli/sqli_lexer.re2c
首先是通过sqli_get_token 来获取到上下文的的一个Token 那么这个Token 是怎么产生的。如下
static int
detect_sqli_add_data(struct detect *detect, const void *data, size_t siz, bool fin)
{
unsigned i;
union SQLI_PARSER_STYPE token_arg;
int rv = 0;
// 遍历所有上下文
for (i = 0; i < detect->nctx; i++) {
// 打印一下i对应的start_tok
printf("[DEBUG] 开始检测上下文: %u\n", i);
struct sqli_detect_ctx *ctx = (void *)detect->ctxs[i];
//
int token;
// 如果当前上下文的解析已经完成,则跳过该上下文
if (ctx->res.finished)
continue;
sqli_lexer_add_data(ctx, data, siz, fin);
do {
memset(&token_arg, 0, sizeof(token_arg)); // 清空token_arg结构体
token = sqli_get_token(ctx, &token_arg);
done:
return (rv);
}
以用户输入1′ union select 1,2,3,4 — 为例子
ctx->lexer.instring 为true的状态下
最开始进入到词法分析中
<> {
// 检查是否在字符串处理模式
if (ctx->lexer.instring) {
// 如果在字符串中,设置为字符串处理状态
YYSETCONDITION(sqli_INSTRING);
arg->data.flags = SQLI_DATA_NOSTART;
detect_buf_init(&ctx->lexer.buf, MINBUFSIZ, MAXBUFSIZ);
goto sqli_INSTRING;
}
// 如果不在字符串中,设置为初始状态
YYSETCONDITION(sqli_INITIAL);
goto sqli_INITIAL;
}
那么会跳转到INSTRING 节点处理1
<INSTRING> "''"|'""'|'``' {
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
goto sqli_INSTRING;
}
<INSTRING> ['"`] => INITIAL {
YYSETSTATE(-1);
printf("INSTRING\n");
RET_DATA(DATA, ctx, arg);
}
<INSTRING> ']' => INITIAL {
YYSETSTATE(-1);
RET_DATA(NAME, ctx, arg);
}
<INSTRING,SQUOTE,DQUOTE,BQUOTE,IQUOTE> [\x00] {
if (ctx->lexer.re2c.fin && ctx->lexer.re2c.tmp_data_in_use &&
ctx->lexer.re2c.pos >= ctx->lexer.re2c.tmp_data + ctx->lexer.re2c.tmp_data_siz) {
YYSETCONDITION(sqli_INITIAL);
DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)--;
arg->data.flags |= SQLI_DATA_NOEND;
YYSETSTATE(-1);
RET_DATA(DATA, ctx, arg);
}
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
DETECT_RE2C_UNUSED_BEFORE(&ctx->lexer.re2c);
goto yy0;
}
<INSTRING,SQUOTE,DQUOTE,BQUOTE,IQUOTE> .|[\n] {
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]); // 获取刚匹配的字符、加入到
DETECT_RE2C_UNUSED_BEFORE(&ctx->lexer.re2c); // 标记当前字符已处理
goto yy0;
}
<INSTRING,SQUOTE,DQUOTE,BQUOTE,IQUOTE> [^] {
DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)--;
YYSETSTATE(-1);
RET_DATA_ERROR(ctx);
RET(ctx, TOK_ERROR);
}
那么根据匹配规则。会匹配到 <INSTRING,SQUOTE,DQUOTE,BQUOTE,IQUOTE> .|[\n] 这个接口
这个节点会将1 加入到缓冲区中。然后跳回到初始节点。
现在已经回到了初始化节点。那么又会走到sqli_INSTRING 这个函数。继续处理INSTRING 那么此刻INSTRING节点就是这几条。匹配规则。则会匹配到
<INSTRING> ['"`] => INITIAL {
YYSETSTATE(-1);
printf("INSTRING\n");
RET_DATA(DATA, ctx, arg);
}
这里就会返回一个内容为1 Token为263 的值。对应的是TOK_DATA
并切换到INITIAL 节点
因为已经到了INITIAL 节点。那么处理空格就到了如下的一个块上去了
<INITIAL> whitespace {
DETECT_RE2C_UNUSED_BEFORE(&ctx->lexer.re2c);
goto sqli_INITIAL;
}
处理完空格后还是在INITIAL 节点中
因为INITIAL 节点上面有这个union 的token 就返回了291 这里并且设置了SQLI_KEY_INSTR 指令
<INITIAL> 'UNION'/key_end {
printf("UNION\n");
KEYNAME_SET_RET(ctx, arg, UNION, SQLI_KEY_INSTR);
}
<INITIAL> 'SELECT'/key_end {
KEYNAME_SET_RET(ctx, arg, SELECT, SQLI_KEY_READ|SQLI_KEY_INSTR);
}
现在还是处于INITIAL 节点匹配到的就是
<INITIAL> '\\'|[0-9] {
printf("NUMBER222\n");
if (ctx->var_start_with_num) {
YYSETCONDITION(sqli_NUMBER_OR_VAR);
detect_buf_init(&ctx->lexer.buf, MINBUFSIZ, 256);
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
goto sqli_NUMBER_OR_VAR;
} else {
YYSETCONDITION(sqli_NUMBER);
detect_buf_init(&ctx->lexer.buf, MINBUFSIZ, 256);
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
goto sqli_NUMBER;
}
}
这里1 已经被写入缓存了
然后跳到了sqli_NUMBER_OR_VAR 那么此刻的值是,
那么就会跳转到
<NUMBER_OR_VAR,NUMBER,DECIMAL,EXP_OR_VAR,EXP> [^] => INITIAL {
DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)--;
YYSETSTATE(-1);
RET_DATA(NUM, ctx, arg);
}
这里就会返回TOK_NUM 266 并且到INITIAL 这个节点
因为已经在INITIAL 节点了 那么,号会跳转到
self = [,\.();=:{}~]; // 所有可能的SQL操作符字符
<INITIAL> self {
printf("SELF\n");
arg->data.value.str = (char *)&selfsyms[DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]];
arg->data.value.len = 1;
arg->data.flags = SQLI_KEY_INSTR;
arg->data.tok = DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1];
RET(ctx, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
}
返回266 和44
opchar = [!^&|%+\-*/<>];
<INITIAL> opchar => OPERATOR {
printf("OPERATOR\n");
detect_buf_init(&ctx->lexer.buf, MINBUFSIZ, MAXBUFSIZ);
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
goto sqli_OPERATOR;
}
<INITIAL> '-' => MINUS {
detect_buf_init(&ctx->lexer.buf, MINBUFSIZ, MAXBUFSIZ);
detect_buf_add_char(&ctx->lexer.buf, DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)[-1]);
goto sqli_MINUS;
}
这里有两个匹配的、那个在前面就会走那个。这里会走opchar 的组。下一跳为OPERATOR
<OPERATOR> '-' {
assert(ctx->lexer.buf.data.len > 0);
if (ctx->lexer.buf.data.str[ctx->lexer.buf.data.len - 1] != '-')
goto opchar_generic;
ctx->lexer.buf.data.len--;
YYSETCONDITION(sqli_DASHCOMMENT);
YYSETSTATE(-1);
if (!ctx->lexer.buf.data.len) {
detect_buf_deinit(&ctx->lexer.buf);
goto sqli_DASHCOMMENT;
}
goto operator_done;
}
这里因为不是结尾了。所以会走到DASHCOMMENT
<DASHCOMMENT> [\x00] => INITIAL {
// TODO: create UNCLOSED_COMMENT key
DETECT_RE2C_YYCURSOR(&ctx->lexer.re2c)--;
goto sqli_INITIAL;
}
触发结尾符。然后状态返回INITIAL最后触发结束符
<INITIAL> [\x00]|'-'[\x00]|'/'[\x00] {
printf("INITIAL -");
if (ctx->lexer.re2c.fin && ctx->lexer.re2c.tmp_data_in_use &&
ctx->lexer.re2c.pos >= ctx->lexer.re2c.tmp_data + ctx->lexer.re2c.tmp_data_siz) {
return (0);
}
printf("yy0 -");
goto yy0;
}
这里是没有返回的。
1. 这个思路可以参考,实际应用当中会有很大的误报需要进行调整词法分析和语法分析
2. 性能较弱
3. 需要增加打分选项
4. 规约冲突 移进冲突较多、需要细化