学习文档
https://codeql.github.com/docs/codeql-language-guides/codeql-for-cpp/
学习文章
基础
安装方法
下载地址
https://github.com/github/codeql-cli-binaries/releases
且安装ql
目录结构
然后按照 vscode 插件
使用准备
学习利用的 测试代码。
1 |
|
需要安装 对应的数据库。
1 | ./codeql/codeql database create --language=cpp -c "gcc hello.c -o hello" ./hello_codedb |
编译且 创建了 hello 文件 -c 指定编译代码需要执行的命令命令
打开对应的 hello_codedb 数据库
选着刚刚的 hello_codedb 文件。
且我们需要选中这个 db 文件
接着我们需要在对应的 语言中创建对应的 hello.ql 文件用于查询
(因为 我们需要 import cpp 所以我们需要在对应的目录下创建 里面有个 cpp.ql 文件
我的对应目录 codeql/ql/cpp/ql/src
测试
hello.ql
内容
测试
1 | select "Hello World!" |
运行结果
1 | import cpp |
运行结果
基础内容
- import 引入ql 文件。
- from 限定基础范围,且可以对其简单命名
- where 限制 ql 查询范围 添加限制
- select 搜索内容 进行查询
库内容
Macro 对应的 宏相关的包
Function 对应 函数调用
FunctionCall 涵盖所有的函数调用,因此我们可以通过该对象来获取特定函数被调用的位置
语句分析
在 codeql 中我们也可以定义属于自己的 谓语 predicate
(函数),利用 predicate 来定义
结构为
1 | predicate name(type arg) |
包含
对应的关键字 predicate
(无返回值)或则利用 返回值类型定义对应的 谓语 (函数)
谓语(函数)的名称 name
谓语(函数)的参数 type arg
谓语(函数)的主题语句
谓语
对于没有返回值的谓语(函数),我们利用 predicate
来定义对应的谓语(函数)类型
对应有返回值的谓语(函数),这里我们利用返回的 对应的 类型来定义 int
等,且这里利用的返回保存在 result
中
对于我们谓语(函数)中的参数,他的数据类型必须是有限的,函数中必须限定他的范围
如
1 | import cpp |
这样调用的时候会报错说我们的数据没有限定对应的范围
如果想要能够利用这个函数。
我们要限定对应的 x y 的范围 可以利用 bindingset
来修饰我们的 x 和 y
1 | import cpp |
从而能够成功
查询
可以利用 as
来修改对应的 参数名字
对于我们的 查询可以利用 order by
来进行排序
类
在 Codeql 中 一个类的创建,其实不是创建一个新的对象,而是标识特定的一类数据集合。
包含
关键字 class
类名 且类名开头字母 必须为 大写
扩展类型 (类名后)如 extends int
即boolean
、float
、int
、string
以及date
。
类主体
例子
1 | class OneTwoThree extends int { |
特征谓语 类似于c++ 的构造函数
1 | OneTwoThree() { // characteristic predicate |
this
变量表示的是当前类中所包含的数据集合。与result
变量类似,this
同样是用于表示数据集合直接的关系。
成员谓语 类似于 对应的 基础函数
1 | string getAString() { // member predicate |
这里可以对传入的数据调用这个函数,且能够函数拼接
类似
1 | class OneTwoThree extends int { |
返回结果
直接调用 getAString 方法,且可以拼接 toUpperCase()
函数来实现 讲返回字符串大写
类字段
从而创建字段
1 | class SmallInt extends int { |
需要注意的是,
- 每个类都不能继承自己
- 不能继承final类
- 不能继承不相容的类
且对于类 也存在 abstract
修饰的抽象类
可以一次继承多个类型
数据流
数据流分析用于分析 变量在代码中各个点的值,在 CodeQL 中,你可以对本地数据流和全局数据流建立模型。
局部数据流
在一个单独的函数中的数据流,简单且比全局更精确。
局部数据流的相关库在 DataFlow 模块中,它被定义为 Node 类,表示数据可以流经的任何元素。Node 被分为表达式 (ExprNode) 和参数 (ParameterNode) 两种。使用成员谓词 asExpr 和 asParameter 可以在数据流 node 和表达式 / 参数 node 之间进行映射
1 | class Node { |
或则使用
1 | /** |
注意:参数结点ParameterNode
指的是当前函数参数的数据流结点。
谓词localFlowStep(Node nodeFrom, Node nodeTo)
可以分析出从nodeFrom
到nodeTo
中的元素之间数据流动的方式。该谓词可以通过使用符号+
和*
来进行递归调用,或者使用预定义好的递归谓词localFlow
。
例如,从零个或多个局部步骤中查找从参数 sourece 到表达式 sink 的污染传播:
1 | TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink)) |
在 codeql 中 如果我们想查询一个 基本操作的 +
加法运算
1 | x = x+1; |
我们可以直接利用 AddInstruction
直接找到 (根据编译的中间代码 IR 进行查找
CodeQL U-Boot 挑战
https://lab.github.com/GitHubtraining/codeql-u-boot-challenge-(cc++)
拷贝创建的 code-uboot 文件
git clone https://github.com/0xC4m3l-jiang/codeql-uboot.git
拷贝 vscode-codeql-starter
git clone --recursive https://github.com/github/vscode-codeql-starter.git
下载对应 u-boot 库
运行环境配置
首先将 codeql-uboot
放入到 vscode-codeql-starter
文件夹下
然后 利用 vscode 点击 文件->打开工作区
选中 vscode-codeql-starter
文件夹中的 vscode-codeql-starter.code-workspace
。
从而打开工作区
然后在 vscode 里面的 codeql 中 选中 u-boot
数据库
这样就配置好了环境
Challenge C++
函数查找
利用 codeql 查询里面存在的函数
1 | import cpp |
通过这个语句我们可以查询到我们的 u-boot
里面存在的对应的函数且可以通过点击确定他的位置
宏查询
利用 Macro
类进行对 u-boot
中宏的查询。这里我们要查询 ntohl, ntohll, ntohs
这里 codeql 可以利用 正则匹配
1 | import cpp |
两个变量查询调用该函数的位置
利用 from 一次创建多个变量
在 where
中添加多个 多个条件进行查询,每个条件中利用 and 进行一个连接
1 | import cpp |
FunctionCall
将会涵盖所有的函数调用,因此我们可以通过该对象来获取特定函数被调用的位置。
Function
对应 函数调用
查看宏的顶层表达
通过 MacroInvocation
这个类来查询 “ntohs” 等宏的调用,并通过 getExpr()
这个方法进行宏的展开,得到相应的代码片段。
1 | import cpp |
实现一个类进行查找
一个类对应一个 数据集合
这里我们可以利用类来限定我们的查询的值
对于一个量的查询 exists
量创建一个临时变量
1 | import cpp |
这里创建 NetworkByteSwap
类 且 继承了 Expr
里面的 特征谓语 NetworkByteSwap()
中限定了。
且 this 指向我们这个集合的内容
污点分析
https://codeql.github.com/docs/codeql-language-guides/analyzing-data-flow-in-cpp/
利用了 数据流有关知识
1 | import cpp |
CodeQL securityLab 挑战
分析 codeql 语句
Github 链接
https://github.com/github/securitylab
下载后可以添加到 vscode-codeql-starter
中
git clone https://github.com/github/securitylab.git
准备
首先我们学习
securitylab/tree/main/CodeQL_Queries/cpp/MinIntNegate
下的实例 ql 语句
MinIntNegate
1 | codeql database create MinIntNegateDB --language=cpp --command="g++ -c test.cpp" |
然后将生成的 MinIntNegateDB
加入到 VSCODE 的 codeql 数据库中
漏洞模式
有符号数的判断问题
下面的代码是对 <0 的数将其转化为 正数的操作
但是如果 x = 0x80000000 这样的清空他一样为 0 这时候改变 x 的符号不会真的改变 x 的正负,也就是说 x 的值还是为 0x80000000 然而这里我们对应的返回值类型是无符号类型,那么返回的值就会是一个很大的正数。
1 | unsigned int test000(int x) { |
编写 codeql 查询上述类型漏洞
查询规则说明
这里用到 Guards库
该类表示用于制定控制流决策的布尔值。
根据上面的实例代码,我们知道 存在漏洞的 x参数,首先会有一个 if 语句的判定 x < 0
- 左边为 变量(x)
- 右边为实数(0)
- 中间有一个操作符逻辑运算。
然后是判断 if 语句后
- 存在一个基本块
- 基本块中有
-
负号计算 - 且
-
负号控制我们的 变量 (x)
测试1
1 | import cpp |
但是我们会发现,这里我们codeql 查询的内容中
左右两边的 参数不一样说明查询不准确这个值查询的 对应变量的 参数名是否一样,没有匹配到是否是同一个类里面的变量。
因为这里我们利用的是 Variable v
变量类型
测试2
这里我们要修改 Variable v
为 LocalScopeVariable
局部变量
1 | import cpp |
这次查到了两个数据
而且 还过滤掉了 s->myfield = -s->myfield;
这个正确的情况 这个情况为一个 全局变量所以不能被查询到。
测试3
因为我们还要查找 s->myfield = -s->myfield;
所以我们需要查询下全局变量
这里可以利用 globalValueNumber
获取表达式的全局值编号
这个函数来对 运算符左右两边的 表达式进行一个处理 然后判断是否相等。这样就能确定 是否能够使用
1 | import cpp |
这样我们就能查询到对应的 左右相同的了
这样我们就能查到对应的内容了,但是这样我们只查询到 小于的时候的比较这里没有 大于比较的时候
这里说明我们还要加 条件实现
测试4
写一个谓语(函数)来进行判断 等于或则大于
1 | import cpp |
利用 ComparisonOperation.qll
里面的谓语 用于匹配 大于比较 或则小于比较
测试5
现在我们添加一个新的内容,让其也支持 大于等于 小于等于
利用 ComparisonOperation.qll
里面谓语
1 | import cpp |
测试6
这里利用的递归进行查找的
1 | import cpp |
调用后
测试7
这里还有一种情况可能
现在外存在一个 bool 值赋值,然后在带入到了 if 语句里面判断是否在一个基础块中,如果要查询这类值,我们需要进行污点流跟踪分析。
1 | exists(Expr prev | |
完整为
1 | import cpp |
查询后得到的结果
测试8
然后我们会发现我们的测试数据中还有其他的情况
主要是 b = !(0 <= x)
这个我们需要先判断是否为 ! 修饰的类型,如果是在对其中的内容进行一个递归判断。
1 | import cpp |
查询到 12 个结果
ChakraCore-bad-overflow-check
资料
https://help.semmle.com/QL/ql-training/cpp/bad-overflow-guard.html
思路
检测对应的整数溢出,整数溢出是在错误使用类型转换的时候所导致的。
有符号 与 无符号数进行运算会产生这样的问题等
例如
1 | bool checkOverflow(unsigned short x, unsigned short y) { |
x+y 会隐式转化为 int 类型。
1 | bool checkOverflow(unsigned short x, unsigned short y) { |
这里强制转换 但是如果 x+y 大于16位 会讲更高位 截取掉可能会产生错误影响。
测试1
首先我们要找到 一个加法运算基本快,一个变量,一个关系运算(大于小于等)
1 | import cpp |
测试2
然后我们现在已经能得到 (a+b<a)这样情况了。
但是还需要另一个先知条件,我们的 a 为16位的类型 也就是对应 4字节
这里我们可以设置一个 谓语来实现
1 | import cpp |
测试3
加法运算的结果不执行强制类型转换,或者强转后的大小大于32位
这个条件会使得溢出检测算法无效,而这就是我们的目标所在。
1 | import cpp |
也有另一种写法
1 | import cpp |
注意where语句中使用的一个通配符_
,该通配符用于表示任何数据集。
Facebook_Fizz_CVE-2019-3560
资料
https://securitylab.github.com/research/facebook-fizz-CVE-2019-3560/
思路
该漏洞是由于PlaintextRecordLayer.cpp的+=
第42行上的整数溢出引起的:
1 | auto length = cursor.readBE<uint16_t>(); |
漏洞点为
这里的 auto length 的值是我们可控的然后下面的 if 语句比较,这里没有进行判断是否存在溢出,而是检测的 buf是否接收了 足够多的数据。
然后会有一个 length +=
操作 这个操作里面 += 后可能会存在整数溢出从而造成 +=
后值为 0
之后会将 length 作为参数传入 buf.trimStart(length);
函数
在这个函数中 会更具 amount
来根据偏移进行,从而造成 指针不被修改
在循环的下一次执行中,cursor会被设置为与当前循环相同的cursor,然后读取与当前循环相同的length,之后length继续溢出至0,buf的指针仍然没有被修改。如此循环往复,程序将陷入循环中无法跳出,这样便造成了拒绝服务攻击(DoS)。
测试1
首先,我们最基础的功能就是要找到在整个代码中,产生了 从较大类型到较小类型的所有转换
这个转换可能会存在溢出的风险,利用到 IR 包
1 | import cpp |
这样能查出很多 可能存在因为类型转换而产生漏洞的地方。虽然存在可能利用的漏洞点,但是我们还需要知道的是,这些存在转换的内容是否是我们能控制的
测试2
我们已经设置好 能知道到 对应存在 类型转换的地方了,现在要做的就是找到这些函数中,哪儿些的参数是我们可控的输入
对于Fizz,事实证明,不受信任的输入是通过另一个名为Folly的Facebook库到达的。Folly将数据放在中
IOBuf
,然后由Fizz读取。因此,建模不受信任数据源的一种方法是找到IOBuf
Fizz中s的所有用法。但是我们发现了另一种解决方案,该解决方案对于Fizz项目既简单又不那么具体:通过套接字发送数据时,通常以网络字节顺序发送数据。因此,通常需要使用或将网络数据转换为主机字节顺序。ntohs
ntohl
。唯一的障碍是Fizz不使用
ntohs
andntohl
!而是使用Endian
类。以下QL类标识Endian
用于将网络字节顺序转换为主机的方法
查找所有Endian::big
函数声明的QL代码
1 | class EndianConvert extends Function { |
我们需要找到这个 Endian::big
的 FunctionCall
因为我们的恶意输入 就是从这个函数产生的
1 | /** |
接着我们要利用跟踪数据流的方法,我们要找到 是否有我们可控数据流能流入 存在 大位进制转小位进制
的地方。利用到 污点流量的 isSource
isSink
1 |
|
最终为
1 | /** |
从而找到存在 漏洞的点
libssh2_eating_error_codes
资料
https://blog.semmle.com/libssh2-integer-overflow/
该漏洞是越界读取,可能导致远程信息泄露。使用libssh2连接到恶意SSH服务器时将触发该事件。libssh2接收uint32_t
来自恶意服务器的,并且对此没有任何限制。然后,libssh2从所指定的偏移量中读取内存uint32_t
。
思路
这里利用的是 调用一个函数后 返回有个 数值的比较 ,且函数返回的值 为 负数 却赋值给了 一个无符号数。
如
1 | int f(){ |
这样运行后 res 会变成一个很大的值
如果 这个 f()
函数目的是输出 一个大小,如果函数错误才会返回 -1 。这样就可能会存在一定的漏洞点,从而可以实现利用。
函数一
1 | int _libssh2_check_length(struct string_buf *buf, size_t len) |
函数二
1 | if((p_len = _libssh2_get_c_string(&buf, &p)) < 0) |
测试1
这里我们要实现的目的为:
- 函数返回为 有符号数 (可以返回 为 -1)
- 赋值的参数为 无符号类型
满足条件1 可以找到 代码中 所有的 能返回 小于0 的地方
1 | from ReturnStmt ret |
找到 对应的有返回值为 小于0 的地方后
我们向上找到 返回这个值的函数
1 | import cpp |
然后继续满足第二个 条件,返回值赋值给了 一个无参数类型的值。
1 | import cpp |
这样 就能进行查找了
这样就能查找到很多 瞒住这种情况的 时候。
测试2
但是后面我们会发现,这个还不能找到
如下 先返回给一个 有符号型,然后在赋值给一个 无符号数的情况。
1 | int res1 = f(); |
这个 情况,我们就需要
利用污点分析的。
1 | import cpp |
还有一种
1 | import cpp |
rsyslog_CVE-2018-1000140
资料
this snapshot db 文件下载
https://github.com/github/securitylab/tree/main/CodeQL_Queries/cpp/rsyslog_CVE-2018-1000140
https://help.semmle.com/QL/ql-training/cpp/snprintf.html#2
https://securitylab.github.com/research/librelp-buffer-overflow-cve-2018-1000140/
思路
这个 evc 的爆出 主要是因为 snprintf
这个 函数引起的
1 | int snprintf(char *str, size_t size, const char *format, ...); |
如果说我们使用的格式化字符串为”% s”,然后在后面的参数中传了一个很长的字符串进去 (大于 size),那么多余的部分虽然会被截止掉,但是 snprintf 的返回值并不是 size 而是参数中字符串的长度。所以当我们这样使用 snprintf 的时候就有可能会造成溢出:
1 |
|
其中
1 | printf("Hello %s!", name) |
在漏洞中 snprintf
可能会返回 -1
测试1
首先我们要找到 这个 snprintf
这个函数
1 | import cpp |
测试2
我们要找到 代码中 snprintf
这个函数的返回值被储存在变量的情况。
利用 where 中的 not call instanceof ExprInVoidContext
来记录
找到所有对应 snprintf 返回值作为变量赋值的地方
1 | import cpp |
测试3
然后我们需要了解到 这个漏洞的产生
是利用了 snprintf
的第3个参数 存在 %s
格式化字符串
我们要找到对应的地方
.regexpMatch("(?s).*%s.*")
利用正则
1 | import cpp |
测试4
接着 返回值又一次作为了 snprintf 的第二个参数,所以我们根据这个条件过滤一下就可以了
这里需要利用 污点分析的方法来搜索
1 | import cpp |
这样 我们就能找到存在漏洞的地方了
可以添加 upperBound(call.getArgument(1).getFullyConverted())
来显示 溢出的大小