CodeQLU-BootChallenge(CC++)

[toc]

# 背景

需要在 U-Boot 中寻找一组 9 个远程代码执行漏洞
漏洞点位于 memcpy 函数
但并非所有调用 memcpy 函数的都存在漏洞
所以我们需要减少误报率,找到真正存在漏洞的 memcpy 调用

放上题目链接
关于环境搭建根据题目提示就可以顺利完成哦
也可以参考我的文章 "CodeQL for VSCode 搭建流程"
不出意外会放在我的博客中

# Step 3 - our first query

在项目中寻找所有名为’strlen’的函数
语法类似于 sql 语句
import cpp : 导入 c++ 规则库
From Function f1 : 声明一个 Function 类的变量为 f1
where f1.getName() = "strlen" : Function.getName () 顾名思义用于获取此声明的名称,也就是名称和"strlen" 相等的声明会被挑选出来
select f1,"a function named strlen" : select 后接要在 result 中展示的项目,用逗号分隔
3_function_definitions.ql

1
2
3
4
5
import cpp

from Function f1
where f1.getName() = "strlen"
select f1,"a function named strlen"

f67230350390011a65f16d340406022d.png
直接在 main 提交
b20d0a09af05b0e0b6f0889c8bd499fe.png
commit 中查看结果,通过
9c74098f2c390ad1f066fa3cd8fc6349.png

# Step 4 - Anatomy of a query

仿照上一步,在项目中寻找所有名为’memcpy’的函数
4_function_definitions.ql

1
2
3
4
5
import cpp

from Function f
where f.getName() = "memcpy"
select f,"a function named memcpy"

提交查看结果,通过
e7a2871ee72e24c0663d8b9042c8e434.png

# Step 5 - Using different classes and their predicates

自定义规则,查找三个名为 ntohs , ntohl or ntohll 的宏定义
需要一个紧凑的查询,而不是三个查找案例组合在一起
给出以下两种方法

  1. 利用正则表达式
    string 类有一个方法 regexpMatch ,接收器将参数与正则表达式匹配
    那我们需要先找到宏定义,再对该字符串进行正则匹配(使用的 java 的匹配模式)
    5_function_definitions.ql
1
2
3
4
5
import cpp

from Macro m
where m.getName().regexpMatch("ntoh(s|l|ll)")
select m,"macros named ntohs, ntohl or ntohll"

运行
dc5f776d88ef17d90e7be870a333f285.png

  1. 使用集合表达式
    给出的格式:<your_variable_name> in [“bar”, “baz”, “quux”]
1
2
3
4
5
import cpp

from Macro m
where m.getName() in ["ntohs","ntohl","ntohll"]
select m,"macros named ntohs, ntohl or ntohll"

运行后和之前的结果相同,提交通过

PS:
上学的时候为了过考试自学的 c++,就是一些简单的语法
看题目说明也没看明白 ntoh 族函数到底是个啥
后来看见了 swing 的文章
才知道 ntoh 族函数通常用来进行网络字节序到主机字节序的转换
其实自己看到的时候就应该去查的,但是因为对题目影响不大就犯懒没去:-(
以后不能这样了!看见没见过的看不懂的一定要去弄清楚

# Step 6 - Relating two variables

找到所有对 memcpy 函数的调用
先看看给的例子
FunctionCall.getTarget() 查询该函数被调用的位置
直接和 Function 类型的 fcn 对比值,说明他返回的值应该就是 Function 类型(这点在下面优化中会用到)

通过 Function.hasName() 获取方法名

1
2
3
4
5
6
7
8
9
import cpp

from FunctionCall call, Function fcn
where
call.getTarget() = fcn and
fcn.getDeclaringType().getSimpleName() = "map" and
fcn.getDeclaringType().getNamespace().getName() = "std" and
fcn.hasName("find")
select call

如果你想要省略中间变量 Function ,使查询的更加紧凑,可以参考以下两个对比
c1.getClass2() 返回的是 Class2 类型的值,因此可以直接调用 Class2 的方法

1
2
3
4
5
6
7
8
9
10
from Class1 c1, Class2 c2
where
c1.getClass2() = c2 and
c2.getProp() = "something"
select c1


from Class1 c1
where c1.getClass2().getProp() = "something"
select c1

根据以上案例思考
我们需要找到 memcpy 函数被调用的位置,可以使用
FunctionCall.getTarget()
并希望查询更加紧凑,可以直接获取找到的函数的名称并进行判断
FunctionCall.getTarget().getName="memcpy"

6_memcpy_calls.ql

1
2
3
4
import cpp
from FunctionCall functioncall
where functioncall.getTarget().hasName("memcpy")
select functioncall

提交通过
40bdea82763f6ab88adaccc6e47931b4.png

# Step 7 - Relating two variables, continued

寻找所有对 ntoh* 宏定义的调用

这里用到的是 MacroInvocation 这个类,顾名思义就是宏定义调用的类
鼠标悬浮看其注释也能看出来
69d9e0e24f06860213619664c0c91321.png
那么我们就可以通过 getMacro() 寻找被调用的宏定义,并得到返回的 Macro 类型值
再获得找到的 Macro 名称进行正则匹配,即可获得我们想要的结果

1
2
3
4
import cpp
from MacroInvocation macInvo
where macInvo.getMacro().getName().regexpMatch("ntoh.*")
select macInvo

(备注:关于正则表达式,不太会写,找的 java 正则 api 看的。
. 表示匹配除换行符 \n 之外的任何单字符, * 表示零次或多次,
我这里希望得到的结果是以 ntoh 开头的宏定义都会被选中。
如果有不对的地方,还希望可以被提出指正◔ ‸◔)

提交通过
5807d43f1ce8796d351638d6f55cde4a.png

# Step 8 - Changing the selected output

根据提示,使用 getExpr() 这个 predicate
先看看这个 getExpr() 的注释说明
是用来获取宏定义表达式的
如果顶级拓展元素不是表达式,它只是一条语句,将不会被选中列为结果
09d6ad29dcccc9157677864a39460e27.png
使用 select macInvo.getExpr() ,就能获得宏定义调用相关的表达式
8_macro_expressions.ql

1
2
3
4
import cpp
from MacroInvocation macInvo
where macInvo.getMacro().getName().regexpMatch("ntoh.*")
select macInvo.getExpr()

例如点击其中一个结果,就会跳转至下图位置
82697e23ad6a1cd05073bea662213bf8.png
提交通过
839b8030c2474b52816e9afc1212d9a1.png

那么查询表达式和查询调用的区别是啥?
看注释说明,
getExpr() :
Gets a top-level expression associated with this macro invocation,if any.
Note that this predicate will fail if the top-level expanded element is not an expression (for example if it is a statement).
This macro is intended to be used with macros that expand to a complete expression.
In other cases, it may have multiple results or no results.

获取关于宏调用的顶级表达式
注意,如果顶级扩展元素不是一个表达式的话查询将失败(例如,它是一个语句)
此宏用于扩展为完整表达式的宏,在其他情况下可能会有多个结果或没有结果

getMacro() :
Gets the macro that is being accessed.
获取正在访问的宏

即 getMacro() 会获取所有调用的宏,即使他只是一个语句
而 getExpr() 只会获取宏调用的顶级表达式
所以 getExpr() 得到的结果集应该包含于 getMacro() 的结果集
这里放上语句和表达式的区别讨论链接

# Step 9 - Write your own class

首先看看学习 exists 关键词给出的例子:
这个规则只是为了获取不秃头的所有人

不秃头的人都会有头发,那么他们的头发都会对应一个或多个颜色
其中 t.getHairColor() 会返回一个 string 类型的值,例如 "red"
如果我们需要获得不秃头的人,我们并不需要知道他们头发的具体颜色,只需要知道 t.getHairColor() 会返回 string 类型的值即可,因为秃头 getHairColor() 时,不会返回任何值

所以我们利用 string 类型的变量完成该操作
更好的方式是使用 exists 关键词,因为我们只是在 where 中使用该变量
例如, exists(string c | t.getHairColor() = c) 使用了 string 类型的临时变量,用于获取 t.getHairColor() 返回了 string 值的 t ,也就是查询了所有头发颜色的值为 string 类型的人

1
2
3
4
5
6
7
8
from Person t
where exists(string c | t.getHairColor() = c)
select t

/*在CodeQL中,以下代码功能同于以上代码,给出只是为了更好地理解*/
from Person t, string c
where t.getHairColor() = c
select t

再来看看类定义中给出的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or this = 3
}

string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}

predicate isEven() { // member predicate
this = 2
}
}

以上代码定义了一个名为 OneTwoThree 的类,继承于 int
类似于构造函数的部分是 this = 1 or this = 2 or this = 3
文档中解释说明这个类中包括了 1,2,3 这三个值
运行以下规则,可以发现 ott 中确实有 1,2,3 这三个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cpp
/*from MacroInvocation macInvo
where macInvo.getMacro().getName().regexpMatch("ntoh.*")
select macInvo.getExpr()*/
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or 3=this
}

string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}

predicate isEven() { // member predicate
this = 2
}
}

from OneTwoThree ott
select ott

e4a0cc58f70e7e930a95949605394de8.png

其中还有一个熟悉的单词 predicate
这个是在类的主体内定义的谓词,是使用变量来限制类中可能的值的逻辑属性
举个例子,运行以下规则,就会得到值 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or 3=this
}

string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}

predicate isEven() { // member predicate
this = 2
}
}

from OneTwoThree ott
where ott.isEven()
select ott

运行截图:
b7da8682bac3c07ef7a2d8d41cffc090.png
再更改规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or 3=this
}

string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}

predicate isEven() { // member predicate
this = 2
}
}

from OneTwoThree ott
where ott = 2
select ott

他们会得到相同的结果
c4bff2b9bd1c95c4becc00965b3a52f4.png
也就是说 where ott.isEven() 和 where ott = 2 做出的是相同的限制
那么我们也就能更好地理解, predicate 特征是用于限制类中可能值的逻辑属性了

其中 string getAString() 就不必多说,返回一个字符串,其中包含对应值
1da4e1a23a80b04ee390a564d069fc2e.png
其中我发现一个很神奇事,不知该如何解释
我将代码中 this=1 改成 1=this 也会得到一样的结果,没有任何不同或报错
它和赋值语句不同,但好像又具有相似的功能
在对变量做限制时,例如 where ott = 2 ,它就变成了一个符号,用于对两个值进行比较,这里还好理解,因为 sql 语法类似
但是同样在以下代码中

1
2
3
predicate isEven() { // member predicate
this = 2
}

this=2 也是用于对两个值进行比较
我认为这是由于 predicate 带来的改变,使得其中的代码和 where 后的代码具有相同得到功能
如果有更好的见解,还不忘赐教

最后来写题
题目给了模板和提示
按照 step8 中的规则进行编写, exists 第二个参数放上 step8 中的 where 条件
由于 select 由题目给出并为 Expr 的子类,所以我们需要增加一个条件获取宏调用相关表达式
根据以上 exists 案例可知,我们需要在 mi.getExpr() = 后面写出他返回值的类型,这样当 mi 为表达式时,就会被选中
NetworkByteSwap 是 Expr 的子类,因此

9_class_network_byteswap.ql

1
2
3
4
5
6
7
8
9
10
import cpp

class NetworkByteSwap extends Expr {
NetworkByteSwap() {
exists(MacroInvocation mi | mi.getMacro().getName().regexpMatch("ntoh.*") | mi.getExpr() = this)
}
}

from NetworkByteSwap n
select n, "Network byte swap"

# Step 10 - Data flow and taint tracking analysis

最后一步,进行数据流分析

先了解以下我们需要查询的函数背景, ntoh* 函数会返回一个数,并用于 memcpy 的第三个参数 size ,所以我们需要追踪的数据流就是从 ntoh* 到 memcpy

在 C/C++ 写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。 这时就可能用到 htons (), ntohl (), ntohs (),htons () 这 4 个网络字节顺序与本地字节顺序之间的转换函数

memcpy 指的是 c 和 c++ 使用的内存拷贝函数,memcpy 函数的功能是从源 src 所指的内存地址的起始位置开始拷贝 n 个字节到目标 dest 所指的内存地址的起始位置中

创建 Config 类,查找此类的数据流并进行污染点追踪分析
进行数据流分析,我们需要用到,部分代码已经在给出的模板中

1
2
3
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

我们需要写两个 predicate ,一个是来源 isSource ,一个是接收器 isSink

isSource 中我们需要查询 ntoh* 宏定义调用的相关表达式,这一步我们已经在 NetworkByteSwap 中写过了
isSink 中我们需要查询调用 memcpy 函数时,传入的第三个参数 size ,这一步我们需要新增加的步骤是获取参数

弄清楚这些后,在编写规则时,根据提示完善代码
我们就能获得 10_taint_tracking.ql 的答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @kind path-problem
*/

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

class NetworkByteSwap extends Expr {
NetworkByteSwap() {
exists(MacroInvocation mi| mi.getMacro().getName().regexpMatch("ntoh(s|l|ll)") | this = mi.getExpr())
}
}

class Config extends TaintTracking::Configuration {
Config() { this = "NetworkToMemFuncLength" }

override predicate isSource(DataFlow::Node source) {
// TODO
/*获取与此节点对应的表达式(如果有)。
此谓词仅在表示表达式求值值的节点上具有结果。
对于从表达式中流出的数据,例如通过引用传递参数时,请使用asDefiningArgument而不是asExpr。*/
source.asExpr() instanceof NetworkByteSwap
}
override predicate isSink(DataFlow::Node sink) {
// TODO
exists(FunctionCall fc | fc.getTarget().hasName("memcpy") | sink.asExpr() = fc.getArgument(2))
}
}

from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Network byte swap flows to memcpy"

# 传送门

cpp 规则语法说明
Java 正则模式
给出的参考案例:CVE-2018-4259: MacOS NFS vulnerabilties lead to kernel RCE(知识点挺多的)
codeql-swing(swing 的语言云淡风轻,条理清晰,如沐春风,我的的语言阿巴阿巴阿巴)
讨论区