解锁SSA IR代码审计新姿势?牛牛来了!

上午好啊大家

这里是上五休一再上二休七的超级牛(调休版)

先提前祝大家国庆快乐

牛牛这次要给大家介绍一手

“基于代码建模的大型项目代码审计新方法”

⬇️

因为大家更了解 AST,所以我们以 AST 为主语来解释“通用 IR”是什么概念:虽然 Java 的 AST 和 PHP 的 AST 在结构上是不一样的,但是作为基本函数表达,funcName(actualParam1, actualParam2)
这种语言核心表达是不变的。因为虽然结构整体可能不同,但是 AST 在细节行为上基本是一致的。我们可以新设计一个数据结构,把两种 AST 在一些关键行为上统一用我们的新的数据结构进行表达。这种新的结构就是一种 “中间表达 IR”。

因此,N 种语言有 N 种 AST 结构,如果你基于 AST 编写了对 N 种语言的不同的分析方法,那么每种分析方法对应的结构也是不同的,你就要每一种语言都是适配。但是如果N 种复杂的 AST 结构能变成统一的一种中间表达(可理解为“通用 AST”),那么你就只需要探索这 “唯一的一种中间表达” 的行为分析技术了。

如果我们再把这一种统一的“中间表达”变成数据结构保存到数据库的“表”中,那么都不用新创建什么 CodeQL 之类的语言,或者什么冷门 Datalog 语言,数据库本身的查询语句都可以起到一定的作用。

SELECT edges FROM codes WHERE funcname LIKE 'eval%';

当我们了解完背景知识之后,其实已经掌握了核心思路,相当于你已经知道了目前最正统的“五雷正法”的修炼路径。但是修炼路上困难重重,仍然需要大量的“知识”来指导上述思路的落地时间,越是朴素简单的思路动手起来往往难度越高。

第一站就是了解我们说的N种AST都能统一的中间表达:这是本讲中最核心也可能是最有张力的技术,全称是静态单赋值的中间表达(Static Single Assignment Intermediate Representation)。其实单从定义上来说,SSA 应该是一个十分简单的技术:在程序中保证每个变量仅被赋值一次。但是这一句话其实隐含了非常大的信息量。

大多数时候,我们会默认为 SSA IR 是一个完全陌生的知识面,实际不然,在很多现代编译器中,SSA 几乎可以说无处不在(虽然他们实现都不一样,但是大部分都是符合 SSA 定义,并且以实现 SSA 化为一个“噱头”,事实上如果你的语言或者你的 IR 实现了 SSA 化,至少意味着它相对在技术上是成熟的,具备极强的扩展能力和自证能力):

LLVM 中有两种 SSA 的应用,一种是本身 LLIR 就是一种 SSA 的表现(尽管在内存使用上使用 Load 和 Store 去绕过 SSA 很难表达的指针操作);另一种是对内存块操作是一个独立的 SSA 指令处理,可以非常好地优化内存中未使用的模块(和死代码删除是同样的思路和技术)。

Golang 的官方实现虽然不接如 LLVM,但是它自己包含了一个 SSA IR 系统;

强如 Rustc 的 MIR(Middle-level IR)使用了类似 LLVM 的非严格 SSA 的系统,可以方便的转换成 LLVM IR 中的 alloca 指令,rustc_mir_transform::ssa 模块实现了 MIR 局部完全 SSA 化。

V8 引擎的 TurboFan 使用 SSA 来直接简化 Use-Def 链,保证了每个 TurboFan 中的值只有一个单一来源,并且快速构建“节点之海”。

仅被赋值一次意味着在大部分时候,数据流都是单向的可以追踪的,你可以明确知道一个变量有没有消亡。SSA 可以回答:“此变量 foo 非彼变量 foo。”

我们可以以最简单的一个案例来理解这种变化:

a = 1;
a = 2;
eval(a);

上述代码中,在 SSA 的理解下,a变量的值一开始被赋值为 1,后赋值为 2。然后被eval(a)执行了。那么我们追踪程序的时候,从 eval 开始寻找数据,从 AST 角度来说,会发现 a 可能有两个值,分别是 1 还是 2。那么我们怎么准确找到 a 对应的是 2 呢?有两个理解:

如果基于AST考虑,那么此时你肯定会想,我需要寻找离eval最近的一个 a 的值,找到了值为 2;

如果基于符号表来考虑,a在赋值的时候被创建过一次,每次赋值都会覆盖之前的,只需要记录赋值的表,再按a这个变量名取出来就好,确定是 2。

这个问题非常简单,但是看起来好像不需要SSA也能做到,那么我们再看下一个案例。

a = 1;
if (conditionB) {
a = 2;
}
eval(a);

上述代码中,在 SSA 的理解下,a变量的值一开始被赋值为 1,随后经过 IF,如果条件为真,被赋值为 2。然后被 eval(a)执行了了。那么我们追踪程序的时候,从 eval 开始寻找数据,从 AST 角度来说,会发现 a可能有两个值,分别是 1 还是 2。那么我们怎么准确找到 a 对应的是 2 呢?有两个理解:

如果基于 AST 考虑,那么此时你肯定会想,我需要寻找离 eval最近的一个 a 的值,但是我们不确定 conditionB 到底是什么,所以无法确定;

如果基于符号表来考虑,a 在赋值的时候被创建过一次,每次赋值都会覆盖之前的,只需要记录赋值的表,再按 a 这个变量名取出来就好,但是此时的表是动态的,也无法确定。

那这种情况,我们如何表达这种 “可能性”呢?至少到这里,传统的 AST 表达方式略显匮乏,到了“只能意会”的阶段了。

在讲解上述问题的解法的时候,我们来完整观察一下 SSA 如何解决那个“简单问题”,根据定义来说,一个变量不能被赋值两次,那么上述代码就应该被修改为符合 SSA 格式的才能继续:

a = 1;
a_1 = 2;
eval(a_1);

我们不需要计算距离,也不需要打变量表,就可以直接得到 eval() 执行的结果必定是 a_1。在对比原来的代码的时候,如果变量名相同,则创建一个原来变量的新版本,用新版本承载新值。

带条件分支的代码,将会被修订为如下版本

a = 1;
if (conditionB) {    
a_1 = 2;
}
a_2 = phi(a, a_1);
eval(a_2);

上述代码引入了一个 SSA 的用来表示“数据流合并”的运算符:phi

我们发现,上述的 a 的值其实会有三种情况:

  1. 初始值:1
  2. 条件语句 TRUE 分支内的值:2
  3. 条件语句结束后的值:phi(a, a_1) 即:可能是 1 可能是 2;

因为 eval 执行的位置是条件语句结束后,所以eval的参数一定为 phi(a, a_1);

虽然我们无法确定到底是 1 还是 2 但是获得了一个精准的表达。

  1. 如果所有的代码都是 SSA 格式的,那么所有数据都会被精确确定;
  2. 如果一个数据有多个可能性,SSA 的 Phi 函数可以准确表达它在数据上可能性;
  3. 天生可以做死代码优化,无需额外算法(案例);
  4. SSA IR 可以让代码更快收敛,直接得到不动点;

  1. 基础设施匮乏,教科书陈旧,现存数据和文章存在大量错误会误导读者,需要参考大量现有的优秀实现和散落在各种地方的知识,并且很多细节需要发挥想象力。
  2. SSA 多被设计为为了优化程序执行,理清数据流的结构,而并不是分析代码行为的结构;
  3. SSA 的编译没有统一的约束和最佳实践,各个语言的 SSA 都不一样,例如:Golang SSA,LLVM SSA就是完全不同的目的,他们的实现和对 SSA 的忠诚度不一样;
  4. 内存操作会产生隐性(间接)赋值,SSA 对内存分配的处理需要额外花一些精力维护隐性(间接)和直接赋值之间的关系,会因为“隐性赋值”产生额外代码;(举例可以理解一下:Golang 中的 “指针/引用” 的处理。)
  5. 现有的很多高级语言特性甚至一些经典的编程模式对 SSA 的实现有非常大的挑战,如何转换成了非常难的问题:例如有类语言到无类语言的等价变换,闭包也属于间接赋值,处理的时候非常复杂。

在我们的最终实现中,设计并总结出26个元指令,通过这 26 个元指令基本可以包罗万象展示市面上语言的 99% 甚至更多的语言行为。

听起来有点离谱吧?但是你想想汇编也是一样的,语法变化千秋万代,CPU 指令集确实有限个数。

我们用一张图直观表达这个事情的最终结果:

操作码用途说明
SSAOpcodeAssert调试用于在代码中插入断言,检查某个条件是否为真
SSAOpcodeBasicBlock控制流用于表示基本的代码块,通常包含一系列的指令,没有内部的控制流
SSAOpcodeBinOp计算用于执行二元操作,如加、减、乘、除等
SSAOpcodeCall函数调用用于调用函数或方法
SSAOpcodeConstInst常量用于表示常量值
SSAOpcodeErrorHandler错误处理用于处理异常或错误
SSAOpcodeExternLib外部库调用用于调用外部库的函数或方法
SSAOpcodeIf控制流用于执行条件判断
SSAOpcodeJump控制流用于执行无条件跳转
SSAOpcodeLoop控制流用于执行循环操作
SSAOpcodeMake对象创建用于创建新的对象或数据结构
SSAOpcodeNext控制流用于跳转到下一个指令
SSAOpcodePanic错误处理用于处理不可恢复的错误情况
SSAOpcodeParameter函数参数用于表示函数或方法的参数
SSAOpcodeFreeValue自由变量(闭包支持)一般出现在一个函数捕获了外部的变量,并且这个外部变量不是形式参数
SSAOpcodeParameterMember对象访问用于访问形参的某个成员
SSAOpcodePhi控制流用于合并来自不同基本块的值
SSAOpcodeRecover错误处理用于从错误或异常中恢复
SSAOpcodeReturn函数返回用于从函数或方法中返回值
SSAOpcodeSideEffect副作用用于表示可能有副作用的操作
SSAOpcodeSwitch控制流用于执行多路分支
SSAOpcodeTypeCast类型转换用于执行类型转换
SSAOpcodeTypeValue类型值用于表示类型值
SSAOpcodeUnOp计算用于执行一元操作,如取反、递增、递减等
SSAOpcodeUndefined未定义用于表示未定义的值或状态
SSAOpcodeFunction函数定义用于定义函数或方法

类比于一个内容管理系统,我们把文章存储到数据库,就可以通过“模糊搜索”或者“精确搜索”来精准找到我们感兴趣的文章内容。
我们把代码 IR 保存到数据库,就可以通过搜索数据库直接去探索代码的行为。因此,建模的数据库只要能保证忠实地记录编译成 IR 的代码行为,并且能让每个 IR 的字节码对应到具体的编译前的代+码中,这样仅需要简单的数据库查询语句就可以找到你想要的代码行为了。

想要达到工程使用的程度,建模数据库并不是一个简单的操作,还需要解决很多棘手问题,我们在前面的内容中,通过 SSA Opcode 的实现图,可以大致明白,SSA 指令之间的关系非常复杂,他有多层级,比如说:Function 指令中一般至少会包含:形式参数(Parameter),基本块(BasicBlock),返回值(Returns)。因为闭包机制,函数会额外产生变量捕获的问题,如果修改了捕获的变量,还会在调用位置产生副作用。那么我们在保存这些 IR 的时候,就需要面对第一个问题:
根据图中的写意信息:IR 的 Opcode 有不同的层级,这些层级如何存储在同一张数据库中?

准则一:降维与扁平化

当然,IR 保存需要降维和扁平化,如果你的操作对象是 AST 的话,自然也可以做降维压扁。实际上做降维有个重要准则:可索引,即任何 IR Code 他都唯一对应一个 ID,也就是你看到的任何 ID 都有且只对应唯一的 SSA 指令;

字段名类型描述
idinteger主键,自动递增
created_atdatetime创建时间
updated_atdatetime更新时间
deleted_atdatetime删除时间
program_namevarchar(255)程序名称
versionvarchar(255)版本信息(SCA 用)
source_code_start_offsetbigint源代码开始偏移量
source_code_end_offsetbigint源代码结束偏移量
source_code_hashvarchar(255)源代码哈希值
opcodebigintSSA 操作码
opcode_namevarchar(255)SSA 操作码名称
opcode_operatorvarchar(255)操作符
namevarchar(255)名称
verbose_namevarchar(255)详细名称
short_verbose_namevarchar(255)简短详细名称
stringvarchar(255)字符串
current_blockbigint当前块
current_functionbigint当前函数
is_functionbool是否是函数
formal_argstext形式参数
free_valuestext自由变量值
member_call_argstext成员调用参数
side_effectstext函数包含的副作用
is_variadicbool是否是可变参数函数
return_codestext返回代码
is_externalbool是否是外部的
code_blockstext代码块
enter_blockbigint进入块
exit_blockbigint退出块
defer_blockbigint延迟执行代码块
children_functiontext子函数
parent_functionbigint父函数
is_blockbool是否是块
pred_blocktext前驱块
succ_blocktext后继块
phistext块中直接包含的 Phi 指令
defstext数据流定义
userstext数据流中的使用者
called_bytext调用者
is_objectbool是否是对象
object_memberstext对象成员
is_object_memberbool是否是对象成员
object_parentbigint对象父级
object_keybigint对象键
masked_codestext掩码代码
is_maskedbool是否被掩码
variabletext变量
program_compile_hashvarchar(255)程序编译哈希值
type_idinteger类型 ID
pointtext
pointertext指针
extra_informationvarchar(255)额外信息

准则二:多模块与懒加载模式

虽然有关系型数据库的外键中可以自动查询出来指令相关的内容,但是经过实践,我们仍然非常不建议直接使用“关系”去管理,通过我们的第一准则,可以得到每个指令都是唯一 ID,因此每个指令保存和其他指令的包含或者被包含关系,都可以直接使用 ID 来存储,这样做就可以做到:懒加载(按需加载 SSA 指令),即虽然指令之间有关联,但是为了防止内存炸掉,在编译存储后,查询任何值都只查询一个记录,有需要再去加载其他的记录;
除了本身指令的设计之外,不同语言之间的模块定义多有差别,例如 php 中一般多文件引用只需要寻找 include 或者 require 就可以组合出完整程序,在 Java 中则需要理解 package 的组合问题。

准则三:SSA 字节码可和变量互相对应

多引用:虽然 SSA IR 中不会保存变量信息,编译后的程序也不存在“变量”这个东西,但是程序中大量的变量名需要和现有的 IR Code 做好关联;与此同时,在编译时要对应好源码位置与 SSA Opcode 的对应关系,这样得到审计结果就可以直接获取对应代码的原位置。

在 SSA IR Code 保存到数据库中,想要利用起来,需要设计一套方案并实现对整个代码的查询。为此,我们实现了一门高度结合代码审计行为的 SyntaxFlow 语言。简单理解,在纯数据库模式下,一个 SyntaxFlow 脚本的执行,会把自己编译成多种数据库操作。
SyntaxFlow 的执行过程非常精妙,它本质上和 Yaklang 一样,是一个栈机,可以实现把一个 SyntaxFlow 编译成多条指令,每条指令对应一个或者若干个查询语句或者代码审计步骤。

SyntaxFlow 的设计和 SFVM 的设计非常符合 Yak Project 传统艺能:编译器与代码虚拟机。我们会使用 eBNF 描述的 SyntaxFlow 的语法编译成若干审计字节码 Opcode,这些字节码定义了审计过程中的小行为:

  1. 可以搜索数据库中关键词对应的指令;
  2. 查看某个指令对应的所有函数调用;
  3. 通过关键字筛选并过滤指令;
  4. 查看某一个指令位置的数据流关系;
  5. 把审计出的结果暂存变量;
  6. 迭代器执行所有相关指令;
  7. 通过指令类型过滤数据等;
  8. SCA 与额外文件适配;
  9. ...

基础特性

  1. 代码容错:可以针对不完整的代码进行审计;
  2. 支持精确搜索,模糊搜索,指定方法搜索;
  3. 支持 SSA 格式下的数据流分析;
  4. 支持 Phi 指令处理 IF For 循环等控制流程;
  5. 支持 OOP 编译成 SSA 格式后的搜索;
  6. 支持 Java 注解的追踪与 SSA 实例化,以适应各类注解入口的框架代码;
  7. 支持 Use-Def 链的运算符(向上递归寻找定义,向下递归寻找引用)

高级特性

  1. 通用语言架构:支持 Yaklang / Java / PHP(Alpha*) / JavaScript(ES) Alpha*;
  2. 自动跨过程,OOP 对象追踪,OOP 内方法跨过程,上下文敏感与函数栈敏感特性,可以支持复杂数据流分析;
  3. 编译产物符号化,构建 Sqlite 格式的标准化符号和 IrCode 表,支持中间表达的可视化。
  4. 支持跨过程与数据流可视化(根据 SF 分析过程自动生成),支持数据 Dot 格式的分析步骤图和数据流图


索引与资料查阅

OPCODE 名字含义
OpPass空操作
OpEnterStatement进入语句
OpExitStatement退出语句
OpDuplicate复制栈顶元素
OpPushSearchExact精确搜索并压栈
OpPushSearchGlob模糊搜索并压栈
OpPushSearchRegexp正则搜索并压栈
OpRecursiveSearchExact递归精确搜索并压栈
OpRecursiveSearchGlob递归模糊搜索并压栈
OpRecursiveSearchRegexp递归正则搜索并压栈
OpGetCall获取函数调用
OpGetCallArgs获取函数调用参数
OpGetAllCallArgs获取所有函数调用参数
OpGetUsers获取使用者
OpGetBottomUsers获取底层使用者
OpGetDefs获取定义
OpGetTopDefs获取顶层定义
OpListIndex列表索引
OpNewRef创建引用
OpUpdateRef更新引用
OpPushNumber压入数字字面量
OpPushString压入字符串字面量
OpPushBool压入布尔字面量
OpPop弹出栈顶元素
OpCondition条件判断
OpCompareOpcode比较操作码
OpCompareString比较字符串
OpEq等于
OpNotEq不等于
OpGt大于
OpGtEq大于等于
OpLt小于
OpLtEq小于等于
OpLogicAnd逻辑与
OpLogicOr逻辑或
OpLogicBang逻辑非
OpReMatch正则匹配
OpGlobMatch模糊匹配
OpNot取反
OpAlert输出变量
OpCheckParams检查参数
OpAddDescription添加描述
OpCreateIter创建迭代器
OpIterNext迭代器获取下一个元素
OpIterEnd迭代器结束
OpCheckStackTop检查栈顶元素
OpFilterExprEnter进入过滤表达式
OpFilterExprExit退出过滤表达式
OpMergeRef合并引用
OpRemoveRef移除引用
OpIntersectionRef求交集引用
OpNativeCall原生调用
OpFileFilterReg文件正则过滤
OpFileFilterXpath文件 XPath 过滤
OpFileFilterJsonPath文件 JsonPath 过滤


例如,我们以下面一个案例来解释这个转换究竟是怎么回事儿,以下是一个显然有命令注入的漏洞代码:

package com.test.example;

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.*;

public class CommandInjectionServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String userInput = request.getParameter("command");
        String command = "cmd.exe /c " + userInput; // 直接使用用户输入
        Process process = Runtime.getRuntime().exec(userInput);
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        PrintWriter out = response.getWriter();

while ((line = reader.readLine()) != null) {
            out.println(line);
        }
    }
}

我们编写对应这个漏洞的行为查询可以简单写为:

Runtime.getRuntime().exec(,* as $sink);

这个代码的执行过程为:

sf   0| - enter -
sf   1| check top
sf   1|   |-- >> stack top is nil (push input)
sf   2|   \
sf   3|  push$exact   Runtime isMember[name]
sf   3|    |-- >> pop match exactly: Runtime
sf   3|    |-- result next: Values: 1
sf   3|    |               0 (t24): Undefined-Runtime
sf   3|    |-- << push next
sf   4|  push$exact   getRuntime isMember[key]
sf   4|    |-- >> pop match exactly: getRuntime
sf   4|    |-- result next: Values: 1
sf   4|    |               0 (t26): Undefined-Runtime.getRuntime
sf   4|    |-- << push next
sf   5|  getCall
sf   5|    |-- >> pop
sf   5|    |-- << push len: 1
sf   6|  push$exact   exec isMember[key]
sf   6|    |-- >> pop match exactly: exec
sf   6|    |-- result next: Values: 1
sf   6|    |               0 (t29): Undefined-.exec
sf   6|    |-- << push next
sf   7|  - enter -
sf   8|  getCallArgs  1
sf   8|    |-- -- peek
sf   8|    |-- - get argument: Values: 1
sf   8|    |               0 (t21): Undefined-request.getParameter(Parameter-request,"command")
sf   8|    |-- << push arg len: 1
sf   8|    |-- << stack grow
sf   9|  check top
sf  10|    \
sf  11|    /
sf  12|  update$ref   sink
sf  12|    |-- >> pop
sf  12|    |--  -> save $sink
sf  13|  - exit -
sf  14|  getCall
sf  14|    |-- >> pop
sf  14|    |-- << push len: 1
sf  15|   /
sf  16| pop
sf  16|   |-- >> pop 1
sf  16|   |-- save-to $_
sf  17|

经过基本观察, 我们发现,上述代码中先找到 Runtime,然后找到 getRuntime,在找到 getRuntime() 的调用位置,查看它的后续是否存在 exec 的调用,随后通过 getCallArgs 找到调用的函数参数。

上述过程其实表达的就是 SyntaxFlow 的每一步的执行用到的指令集,他们彼此串联交叉从而得到一个结果。当然上述指令除了功能上的执行之外,获取数据获取 IR 节点都需要在数据库查询,执行完之后,数据库记录的查询语句如下:

SELECT count(*) FROM "ir_indices"  WHERE "ir_indices"."deleted_at" IS NULL AND ((( program_name = 'com.example' ) OR ( program_name = 'test' )) AND (variable_name = 'Runtime' OR class_name = 'Runtime' OR field_name = 'Runtime'))
SELECT * FROM "ir_indices"  WHERE "ir_indices"."deleted_at" IS NULL AND ((( program_name = 'com.example' ) OR ( program_name = 'test' )) AND (variable_name = 'Runtime' OR class_name = 'Runtime' OR field_name = 'Runtime')) LIMIT 100 OFFSET 0
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '24')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '9')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '13')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 19)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '15')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '16')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '17')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '12')) ORDER BY "ir_codes"."id" ASC LIMIT 1
...
...
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '44')) ORDER BY "ir_codes"."id" ASC LIMIT 1
...
...
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '25')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '26')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 15)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '12')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 2)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '12')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 21)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '12')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '30')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '28')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '29')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 12)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '26')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '24')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '12')) ORDER BY "ir_codes"."id" ASC LIMIT 1
SELECT * FROM "ir_types"  WHERE "ir_types"."deleted_at" IS NULL AND (("ir_types"."id" = 2)) ORDER BY "ir_types"."id" ASC LIMIT 1
SELECT * FROM "ir_sources"  WHERE (source_code_hash = '0ef46d9369a641a2a274c753208a7f982b7b376bd1d748b2ec7fa7e6f7e47f7e') LIMIT 1
SELECT * FROM "ir_codes"  WHERE "ir_codes"."deleted_at" IS NULL AND ((id = '11')) ORDER BY "ir_codes"."id" ASC LIMIT 1
...
...


指路:https://github.com/yaklang/syntaxflow

翻阅 SyntaxFlow Cookbook

查询样例规则


当我们 SyntaxFlow 实现完后,我们可以编写一整套规则对同一个项目进行扫描,以此直接获取一个结果,暂时我们以 Java 项目做为案例,为大家讲述一下扫描代码在 SSA - SyntaxFlow 体系中是怎么一回事儿。

上图展示了一些 Java 代码审计规则的分布情况。每个规则都被标记在一个象限中,根据其严重程度和提示信息的详细程度进行分类。

第一象限 - 详细安全评估 亟需修复

这个象限包含了一些严重的漏洞,需要立即修复。这些漏洞可能会导致严重的安全问题,并且提示信息也很详细,容易定位和修复。例如:

  • servlet-command-inject-path: 这个规则检测 Servlet 代码中可能存在的命令注入漏洞,严重程度高,需要重点关注。
  • xxe:saxparser-factory: 这个规则检测 XML 外部实体注入漏洞,也属于高危漏洞,需要尽快修复。

第二象限 - 容易犯错的代码 重点关注

这个象限包含了一些中等严重程度的漏洞,提示信息也比较详细。这些漏洞可能会导致一些安全问题,但不会造成严重的后果。开发人员需要重点关注这些规则,防止在编码过程中引入这些漏洞。例如:

  • filedownload-attachment-filename: 这个规则检测文件下载时文件名的安全性,如果设置不当可能会导致一些安全问题。
  • sqlstring-append-query: 这个规则检测 SQL 语句拼接,如果拼接不当可能会导致 SQL 注入漏洞。

第三象限 - 选择性忽略

这个象限包含了一些低危漏洞,提示信息也比较简单。这些漏洞可能会导致一些安全问题,但不会造成严重的后果。开发人员可以选择性地忽略这些规则,根据实际情况决定是否修复。例如:

  • xss-filter-bypassing-prompt: 这个规则检测 XSS 过滤器的绕过,如果绕过不当可能会导致 XSS 漏洞,但危害较小。

第四象限 - 按需修复

这个象限包含了一些低危漏洞,提示信息也比较简单。这些漏洞可能会导致一些安全问题,但不会造成严重的后果。开发人员可以根据实际情况决定是否修复这些规则。例如:

  • websecurity-csrf-disabled-simple: 这个规则检测 CSRF 防护是否被禁用,如果禁用不当可能会导致 CSRF 漏洞,但危害较小。

规则名规则说明级别类型额外信息
java-servlet-command-inject-path.sfJava Servlet 路径命令注入(仅可达路径)漏洞CWE-77 命令注入
java-servlet-n-spring-directly-command-injection.sfJava Servlet 和 Spring 直接命令注入漏洞CWE-77 命令注入
java-filedownload-attachment-filename.sf文件下载附件文件名信息漏洞
java-command-exec-misc.sf命令执行杂项信息审计lib
java-controller-params.sf控制器参数信息审计lib
java-process-builder.sf进程构建器信息审计lib
java-servlet-params.sfServlet 参数信息审计lib
java-write-filename-params.sf写入文件名参数信息审计lib
java-el-expr-factory-use.sf表达式语言工厂使用信息审计
java-reflection-for-class-unsafe.sf不安全的类反射信息审计
java-spel-parser-injection.sfSpring 表达式语言解析器注入信息审计
java-script-manager-eval.sf脚本管理器评估信息审计lib
java-springframework-config-disable-csrf.sfSpring 框架配置禁用 CSRF信息漏洞
java-xss-filter-bypassing-prompt.sfXSS 过滤绕过提示信息审计
java-jdbc-prepared-execute.sfJDBC 预处理执行信息审计lib
java-jdbc-raw-execute.sfJDBC 原始执行信息审计lib
java-multipart-file-transfer-to.sf多部分文件传输信息审计
java-set-header-filedownload.sf设置文件下载头信息审计
java-spring-resource-handler-location.sfSpring 资源处理器位置信息审计
java-springboot-websecurity-click-hijack-checking.sfSpring Boot Web 安全点击劫持检查信息漏洞
java-springfox-awared.sfSpringfox 使用信息审计
java-sqlstring-append-query.sfSQL 字符串追加查询信息审计
java-websecurity-csrf-disabled-simple.sf简单禁用 CSRF 的 Web 安全信息漏洞
java-saxbuilder-unsafe.sf不安全的 SAX 构建器漏洞CWE-611 XXE
java-saxparser-factory-unsafe.sf不安全的 SAX 解析器工厂漏洞CWE-611 XXE
java-saxreader-unsafe.sf不安全的 SAX 读取器漏洞CWE-611 XXE
java-springboot-filedownload.sfSpring Boot 文件下载漏洞
java-springboot-multipart-files-write.sfSpring Boot 多部分文件写入审计
java-xmlreader-factory-unsafe.sf不安全的 XML 读取器工厂漏洞CWE-611 XXE
java-xstream-unsafe.sf不安全的 XStream 使用漏洞CWE-611 XXE
java-runtime-exec.sf运行时执行审计lib
java-sax-transformer-factory-unsafe.sf不安全的 SAX 转换器工厂漏洞CWE-611 XXE
java-transformer-factory-unsafe.sf不安全的转换器工厂漏洞CWE-611 XXE
java-spring-el-use.sfSpring 表达式语言使用审计
java-freemarker-model-put-params-sink.sfFreemarker 模型参数传递审计
java-mybatis-to-springframework-param.sfMyBatis 到 Spring 框架参数漏洞CWE-89 SQL 注入
END

YAK官方资源

Yak 语言官方教程:
https://yaklang.com/docs/intro/

Yakit 视频教程:
https://space.bilibili.com/437503777

Github下载地址:
https://github.com/yaklang/yakit
https://github.com/yaklang/yaklang

Yakit官网下载地址:
https://yaklang.com/

Yakit安装文档:
https://yaklang.com/products/download_and_install

Yakit使用文档:
https://yaklang.com/products/intro/

常见问题速查:
https://yaklang.com/products/FAQ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值