SQL注入之语义分析
一、语义分析介绍
1.1 介绍
此次分析的语义分析模块使用的是 https://github.com/wallarm/libdetection
libdetection 原理是通过用户输入的字符串 使用词法提取的方式生成词法的Token 并标记好
当前Token的危险等级、和操作符,发送给语法分析, 再由语法分析进行提起符合预设的规则判断为攻击行为。
1.2 与正则、指纹库方案对比
|
名称
|
优点
|
缺点
|
|
语义分析
|
1.准确率高:深入理解 SQL 语句的语义,能准确识别复杂的注入模式,不易被绕过。例如对于利用编码、变形等手段隐藏的注入语句也有较好的检测效果。
2. 适应性强:可以适应不同类型的数据库和 SQL 语法,不局限于特定的规则。
3. 可检测复杂逻辑:能够检测包含复杂逻辑和嵌套结构的注入攻击,如多重条件组合、子查询注入等。
|
1.性能开销大:需要对 SQL 语句进行深度解析和语义理解,计算资源消耗大,处理速度相对较慢,在高并发场景下可能成为性能瓶颈。
|
|
正则表达式
|
1.实现简单:语法相对简单,易于编写和理解,开发成本低。可以快速实现基本的 SQL 注入检测规则。
|
1.规则维护困难:随着攻击手段的不断变化,需要不断更新和完善正则表达式规则,否则容易出现漏判。
|
|
Libinjection指纹库
|
1.轻量级:代码简洁,资源占用少,对系统性能影响小,适合在资源受限的环境中使用。
|
1.规则有限:预定义的规则可能无法覆盖所有的 SQL 注入场景,对于一些新型的攻击手段可能检测能力不足。
|
二、架构
2.1 框架图
2.2 执行流程图
三、源码分析
3.1 处理的总流程图
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]);
}
这个节点直接返回ASCII 编码 44
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;
}
这里是没有返回的。
4.2.9 统计
最终得到的Token序列如下
|
值
|
对应的TOK
|
Token 值
|
|
1
|
TOK_DATA
|
263
|
|
UNION
|
TOK_UNION
|
291
|
|
SELECT
|
TOK_SELECT
|
313
|
|
空格
|
无
|
无
|
|
1
|
|
266
|
|
,
|
44
|
44
|
|
2
|
TOK_NUM
|
266
|
|
,
|
44
|
44
|
|
3
|
TOK_NUM
|
266
|
|
,
|
44
|
44
|
|
4
|
TOK_NUM
|
266
|
|
空格
|
无
|
无
|
|
–
|
无
|
无
|
|
–
|
无
|
无
|
整体向语法检测的输出了如下的Token
[DEBUG] 词法单元: 263 [DEBUG] 词法单元: 291 [DEBUG] 词法单元: 313 [DEBUG] 词法单元: 266 [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266 [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266 [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266
五、语法分析
yacc 代码如下
https://github.com/wallarm/libdetection/blob/master/lib/sqli/sqli_parser.y
5.1 LALR(1)算法介绍
这里采用的LALR(1)算法 它属于自下而上的分析方法
这里使用一个示例文法举例子
E -> E + T E -> T T -> T * F T -> F F -> ( E ) F -> id
自下而上分析过程示例
假设输入的符号串是 id + id * id,下面是使用该文法进行自下而上分析(移进 – 归约)的步骤:
| 步骤 | 栈状态 | 输入符号串 | 动作 |
| 1 | $ | id + id * id $ | 移进 id |
| 2 | $ id | + id * id $ | 归约 F -> id |
| 3 | $ F | + id * id $ | 归约 T -> F |
| 4 | $ T | + id * id $ | 归约 E -> T |
| 5 | $ E | + id * id $ | 移进 + |
| 6 | $ E + | id * id $ | 移进 id |
| 7 | $ E + id | * id $ | 归约 F -> id |
| 8 | $ E + F | * id $ | 归约 T -> F |
| 9 | $ E + T | * id $ | 移进 * |
| 10 | $ E + T * | id $ | 移进 id |
| 11 | $ E + T * id | $ | 归约 F -> id |
| 12 | $ E + T * F | $ | 归约 T -> T * F |
| 13 | $ E + T | $ | 归约 E -> E + T |
| 14 | $ E | $ | 接受 |
每个上下文都会有一个初始的状态。
因为在detect_sqli_start 函数中。
static int
detect_sqli_start(struct detect *detect)
{
unsigned i;
// 遍历所有上下文
for (i = 0; i < detect->nctx; i++) {e_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;
}
}
这个为初始的状态。后续的上下文都是从这个初始的状态开启。进行匹配语法的。
还是用用户输入1′ union select 1,2,3,4 — 为例子
ctx->lexer.instring 为true的状态下
[DEBUG] 词法单元: 263 TOK_DATA [DEBUG] 词法单元: 291 TOK_UNION [DEBUG] 词法单元: 313 TOK_SELECT [DEBUG] 词法单元: 266 TOK_NUM [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266 TOK_NUM [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266 TOK_NUM [DEBUG] 词法单元: 44 [DEBUG] 词法单元: 266 TOK_NUM
5.2 处理 263 TOK_DATA
data: TOK_DATA
这里就直接返回了。因为解析器需要更多令牌,继续处理
5.3 处理 263 TOK_UNION
在处理TOK_UNION 之前。需要向上去找到TOK_DATA 最顶层的规则(文法)
data_name: data
| TOK_NAME
| TOK_DATA2
| TOK_TABLE
| TOK_BINARY
| TOK_OPEN
| TOK_LANGUAGE
| TOK_PERCENT
| '{'[u1] TOK_NAME[name] noop_expr '}'[u2] {
YYUSE($u1);
$$ = $name;
YYUSE($u2);
}
/* Tokens-as-identifiers here */
;
找到了data_name 为什么找到data_name 因为 可以是由一个单独的data 来代表。那么就可以用这个data_name 继续往上找
colref_exact2 因为data_name 单独表示colref_exact2
colref_exact2:
data_name
通过colref_exact2 继续向上找
colref_exact: colref_exact2
| data_name[dname] ':'[u1] colref_exact2 {
printf("经过 colref_exact21111\n");
sqli_token_data_destructor(&$dname);
YYUSE($u1);
}
;
可以找到colref_exact
noop_expr: expr_common
| logical_expr
| colref_exact{
这里logical_expr 可以单独代表noop_expr 那么就可以继续往上找
expr_cont:
| noop_expr after_exp_cont_op_noexpr after_exp_cont
| noop_expr where_opt after_exp_cont_op_noexpr after_exp_cont
;
处理UNION
union_tk:
TOK_UNION {printf("经过 union_tk\n");}
| TOK_INTERSECT
| TOK_EXCEPT
;
union_c: union_tk[tk] {
printf("经过 union_c\n");
sqli_token_data_destructor(&$tk);
}
;
5.4 处理 313 TOK_SELECT
select: TOK_SELECT[tk] select_args into_opt from_opt
where_opt select_after_where {
sqli_store_data(ctx, &$tk);
}
| TOK_SELECT[tk] select_args from_opt into_opt
where_opt select_after_where {
sqli_store_data(ctx, &$tk);
}
| select union_c all_distinct_opt select_parens
| select union_c all_distinct_opt execute
;
这里因为需要传递参数。所以需要等待用户传递的参数
5.4 处理 266 TOK_NUM
noop_expr: expr_common{ printf("noop_expr->expr_common\n");}
| logical_expr{ printf("noop_expr->logical_expr\n");}
| colref_exact{ printf("noop_expr->colref_exact\n");}
| colref_asterisk{ printf("noop_expr->colref_asterisk\n");}
| TOK_NUM {
printf("noop_expr->TOK_NUM\n");
sqli_token_data_destructor(&$TOK_NUM);
}
| noop_expr post_exprs
;
往上追溯noop_expr
select_arg:
noop_expr alias_opt
;
因为alias_opt 是可以为空的。符合select_arg
那么
TOK_SELECT[tk] select_args from_opt into_opt
这条规则就符合from_opt into_opt where_opt select_after_where 这些都是可以为空的。
那么就成立select 这条规则。
debug 如下
[DEBUG] 词法单元: 263 Value: 1 TOK_DATA [DEBUG] 词法单元: 291 Value: UNION 经过 colref_exact2 sqli_token_data_destructor Value: 1 经过 colref_exact: colref_exact2 noop_expr->colref_exact 11dada 经过 union_tk 经过 union_c sqli_token_data_destructor Value: UNION [DEBUG] 词法单元: 313 Value: SELECT [DEBUG] 词法单元: 266 Value: 1 noop_expr->TOK_NUM sqli_token_data_destructor Value: 1 [DEBUG] 词法单元: 44 Value: , select_arg->noop_expr 经过 select_list [DEBUG] 词法单元: 266 Value: 1 noop_expr->TOK_NUM sqli_token_data_destructor Value: 1 select_arg->noop_expr 经过 27222 经过 select2 sqli_store_data: unknown token 313 sqli_store_key1: READ sqli_store_key2: SELECT detect_ctx_result_store_token1: READ sqli_store_key1: INSTR sqli_store_key2: SELECT detect_ctx_result_store_token1: INSTR sqli_token_data_destructor Value: SELECT ---------------------------end----------------
六、总结
1. 这个思路可以参考,实际应用当中会有很大的误报需要进行调整词法分析和语法分析
2. 性能较弱
3. 需要增加打分选项
4. 规约冲突 移进冲突较多、需要细化





