SQL注入之语义分析
文章描述了SQL注入检测系统的实现过程。系统通过红黑树存储解析器模块,并初始化这些模块。每个解析器模块由detect_parser结构体定义,包含多个函数指针用于操作入口。初始化过程中创建并配置检测实例,并为每个上下文分配内存和设置初始状态。词法分析使用re2c生成器处理输入数据,生成Token进行语法分析。文章指出当前实现存在误报率高、性能不足等问题,并建议改进措施如增加评分机制和优化规约冲突处理。 2025-5-12 13:12:40 Author: www.o2oxy.cn(查看原文) 阅读量:4 收藏

3.2 detect_init 初始化

初始化现有的所有的一个解析器模块

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;
};

3.3 detect_open 初始化模块上下文

这里调用的是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);
}

3.4 detect_start 给上下文变量赋值

内置了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

4.1 detect_add_data 添加Token到语法分析中

首先是通过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);
}

4.2 Token 产生的过程

以用户输入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;
}

4.2.1 处理1

那么会跳转到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 加入到缓冲区中。然后跳回到初始节点。

4.2.2 处理’

现在已经回到了初始化节点。那么又会走到sqli_INSTRING 这个函数。继续处理INSTRING 那么此刻INSTRING节点就是这几条。匹配规则。则会匹配到

<INSTRING> ['"`] => INITIAL {
  YYSETSTATE(-1);
  printf("INSTRING\n");
  RET_DATA(DATA, ctx, arg);
}

这里就会返回一个内容为1 Token为263 的值。对应的是TOK_DATA

并切换到INITIAL 节点

4.2.3 处理 空格

因为已经到了INITIAL 节点。那么处理空格就到了如下的一个块上去了

<INITIAL> whitespace {
  DETECT_RE2C_UNUSED_BEFORE(&ctx->lexer.re2c);
  goto sqli_INITIAL;
}

处理完空格后还是在INITIAL 节点中

4.2.4 处理union

因为INITIAL 节点上面有这个union 的token 就返回了291 这里并且设置了SQLI_KEY_INSTR 指令

<INITIAL> 'UNION'/key_end {
         printf("UNION\n");
          KEYNAME_SET_RET(ctx, arg, UNION, SQLI_KEY_INSTR);
      }

4.2.5 处理select

<INITIAL> 'SELECT'/key_end {
          KEYNAME_SET_RET(ctx, arg, SELECT, SQLI_KEY_READ|SQLI_KEY_INSTR);
      }

4.2.6 处理1

现在还是处于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 这个节点

4.2.7 处理,号

因为已经在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]);
}

4.2.7 处理2, 同理如上

返回266 和44

4.2.8 处理–

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. 规约冲突 移进冲突较多、需要细化 


文章来源: https://www.o2oxy.cn/4414.html
如有侵权请联系:admin#unsafe.sh