上午好啊大家
这里是上五休一再上二休七的超级牛(调休版)
先提前祝大家国庆快乐
牛牛这次要给大家介绍一手
“基于代码建模的大型项目代码审计新方法”
⬇️
因为大家更了解 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
- 条件语句 TRUE 分支内的值:2
- 条件语句结束后的值:phi(a, a_1) 即:可能是 1 可能是 2;
因为 eval 执行的位置是条件语句结束后,所以eval的参数一定为 phi(a, a_1);
虽然我们无法确定到底是 1 还是 2 但是获得了一个精准的表达。
- 如果所有的代码都是 SSA 格式的,那么所有数据都会被精确确定;
- 如果一个数据有多个可能性,SSA 的 Phi 函数可以准确表达它在数据上可能性;
- 天生可以做死代码优化,无需额外算法(案例);
- SSA IR 可以让代码更快收敛,直接得到不动点;
- 基础设施匮乏,教科书陈旧,现存数据和文章存在大量错误会误导读者,需要参考大量现有的优秀实现和散落在各种地方的知识,并且很多细节需要发挥想象力。
- SSA 多被设计为为了优化程序执行,理清数据流的结构,而并不是分析代码行为的结构;
- SSA 的编译没有统一的约束和最佳实践,各个语言的 SSA 都不一样,例如:Golang SSA,LLVM SSA就是完全不同的目的,他们的实现和对 SSA 的忠诚度不一样;
- 内存操作会产生隐性(间接)赋值,SSA 对内存分配的处理需要额外花一些精力维护隐性(间接)和直接赋值之间的关系,会因为“隐性赋值”产生额外代码;(举例可以理解一下:Golang 中的 “指针/引用” 的处理。)
- 现有的很多高级语言特性甚至一些经典的编程模式对 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 指令;
字段名 | 类型 | 描述 |
id | integer | 主键,自动递增 |
created_at | datetime | 创建时间 |
updated_at | datetime | 更新时间 |
deleted_at | datetime | 删除时间 |
program_name | varchar(255) | 程序名称 |
version | varchar(255) | 版本信息(SCA 用) |
source_code_start_offset | bigint | 源代码开始偏移量 |
source_code_end_offset | bigint | 源代码结束偏移量 |
source_code_hash | varchar(255) | 源代码哈希值 |
opcode | bigint | SSA 操作码 |
opcode_name | varchar(255) | SSA 操作码名称 |
opcode_operator | varchar(255) | 操作符 |
name | varchar(255) | 名称 |
verbose_name | varchar(255) | 详细名称 |
short_verbose_name | varchar(255) | 简短详细名称 |
string | varchar(255) | 字符串 |
current_block | bigint | 当前块 |
current_function | bigint | 当前函数 |
is_function | bool | 是否是函数 |
formal_args | text | 形式参数 |
free_values | text | 自由变量值 |
member_call_args | text | 成员调用参数 |
side_effects | text | 函数包含的副作用 |
is_variadic | bool | 是否是可变参数函数 |
return_codes | text | 返回代码 |
is_external | bool | 是否是外部的 |
code_blocks | text | 代码块 |
enter_block | bigint | 进入块 |
exit_block | bigint | 退出块 |
defer_block | bigint | 延迟执行代码块 |
children_function | text | 子函数 |
parent_function | bigint | 父函数 |
is_block | bool | 是否是块 |
pred_block | text | 前驱块 |
succ_block | text | 后继块 |
phis | text | 块中直接包含的 Phi 指令 |
defs | text | 数据流定义 |
users | text | 数据流中的使用者 |
called_by | text | 调用者 |
is_object | bool | 是否是对象 |
object_members | text | 对象成员 |
is_object_member | bool | 是否是对象成员 |
object_parent | bigint | 对象父级 |
object_key | bigint | 对象键 |
masked_codes | text | 掩码代码 |
is_masked | bool | 是否被掩码 |
variable | text | 变量 |
program_compile_hash | varchar(255) | 程序编译哈希值 |
type_id | integer | 类型 ID |
point | text | 点 |
pointer | text | 指针 |
extra_information | varchar(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,这些字节码定义了审计过程中的小行为:
- 可以搜索数据库中关键词对应的指令;
- 查看某个指令对应的所有函数调用;
- 通过关键字筛选并过滤指令;
- 查看某一个指令位置的数据流关系;
- 把审计出的结果暂存变量;
- 迭代器执行所有相关指令;
- 通过指令类型过滤数据等;
- SCA 与额外文件适配;
- ...
基础特性
- 代码容错:可以针对不完整的代码进行审计;
- 支持精确搜索,模糊搜索,指定方法搜索;
- 支持 SSA 格式下的数据流分析;
- 支持 Phi 指令处理 IF For 循环等控制流程;
- 支持 OOP 编译成 SSA 格式后的搜索;
- 支持 Java 注解的追踪与 SSA 实例化,以适应各类注解入口的框架代码;
- 支持 Use-Def 链的运算符(向上递归寻找定义,向下递归寻找引用)
高级特性
- 通用语言架构:支持 Yaklang / Java / PHP(Alpha*) / JavaScript(ES) Alpha*;
- 自动跨过程,OOP 对象追踪,OOP 内方法跨过程,上下文敏感与函数栈敏感特性,可以支持复杂数据流分析;
- 编译产物符号化,构建 Sqlite 格式的标准化符号和 IrCode 表,支持中间表达的可视化。
- 支持跨过程与数据流可视化(根据 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.sf | Java Servlet 路径命令注入(仅可达路径) | 高 | 漏洞 | CWE-77 命令注入 |
java-servlet-n-spring-directly-command-injection.sf | Java 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.sf | Servlet 参数 | 信息 | 审计 | lib |
java-write-filename-params.sf | 写入文件名参数 | 信息 | 审计 | lib |
java-el-expr-factory-use.sf | 表达式语言工厂使用 | 信息 | 审计 | |
java-reflection-for-class-unsafe.sf | 不安全的类反射 | 信息 | 审计 | |
java-spel-parser-injection.sf | Spring 表达式语言解析器注入 | 信息 | 审计 | |
java-script-manager-eval.sf | 脚本管理器评估 | 信息 | 审计 | lib |
java-springframework-config-disable-csrf.sf | Spring 框架配置禁用 CSRF | 信息 | 漏洞 | |
java-xss-filter-bypassing-prompt.sf | XSS 过滤绕过提示 | 信息 | 审计 | |
java-jdbc-prepared-execute.sf | JDBC 预处理执行 | 信息 | 审计 | lib |
java-jdbc-raw-execute.sf | JDBC 原始执行 | 信息 | 审计 | lib |
java-multipart-file-transfer-to.sf | 多部分文件传输 | 信息 | 审计 | |
java-set-header-filedownload.sf | 设置文件下载头 | 信息 | 审计 | |
java-spring-resource-handler-location.sf | Spring 资源处理器位置 | 信息 | 审计 | |
java-springboot-websecurity-click-hijack-checking.sf | Spring Boot Web 安全点击劫持检查 | 信息 | 漏洞 | |
java-springfox-awared.sf | Springfox 使用 | 信息 | 审计 | |
java-sqlstring-append-query.sf | SQL 字符串追加查询 | 信息 | 审计 | |
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.sf | Spring Boot 文件下载 | 中 | 漏洞 | |
java-springboot-multipart-files-write.sf | Spring 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.sf | Spring 表达式语言使用 | 中 | 审计 | |
java-freemarker-model-put-params-sink.sf | Freemarker 模型参数传递 | 中 | 审计 | |
java-mybatis-to-springframework-param.sf | MyBatis 到 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