CodeQL之CWE-401(2)
在写第二类
CWE-401
的查询脚本前,我们需要补充一些东西、在能找到的codeql
文档里,大部分都只强推了它的DataFlow
模块,而在写第二类查询脚本时,需要用到它的可达性分析模块import semmle.code.cpp.controlflow.StackVariableReachability
CodeQL可达性分析模块#
在这个模块里,存在三个抽象类StackVariableReachability
、StackVariableReachabilityWithReassignment
和StackVariableReachabilityExt
StackVariableReachability#
StackVariableReachability
存在三个抽象函数,需要自己实现具体内容。isSource
和isSink
很好理解。isBarrier
是指中间不允许出现的某个节点
1 | /** Holds if `node` is a source for the reachability analysis using variable `v`. */ |
同时也有可达性分析的函数reaches
的描述为Holds if the source node can reach the sink node without crossing a barrier
,也就是说可达是指,我从source
点到sink
点,但是不想经过barrier
。
1 | predicate reaches(ControlFlowNode source, SemanticStackVariable v, ControlFlowNode sink) { |
reaches
函数是在一个函数内判断的,它其实是分为两类进行的。
- 第一类:
source、sink、barrier
在一个基本块内。那么就是判断source点、sink都点在基本块内,且满足前后控制流关系,并且不存在一个barrier点。 - 第二类:
source、sink、barrier
是跨基本块的。还是先判断source点存在于当前基本块,然后在它的后继基本块里继续找sink
点和barrier
点,即调用bbSuccessorEntryReaches
函数,具体细节就不多说啦。
StackVariableReachabilityExt#
StackVariableReachabilityExt
和StackVariableReachability
类似,从注释里我们可以看出
1 | /** |
它和StackVariableReachability
的区别在于isBarrier
函数作用于边而不是控制流结点
1 | /** `node` is a barrier for the reachability analysis using variable `v` and starting from `source`. */ |
StackVariableReachabilityWithReassignment#
StackVariableReachabilityWithReassignment
和之前的类似,区别在于这个类将重新赋值的情况考虑了进去
1 | /** |
同时使用reachTo
替代父类的reaches
条件,加入了其余的判断
1 | /** |
进一步熟悉可达性分析模块#
CodeQL开发者在示例代码中有两处可以让我们更好的了解上面的三个类的使用,一个是https://github.com/github/codeql/blob/main/cpp/ql/src/Critical/MemoryMayNotBeFreed.ql
另一个是https://github.com/github/codeql/blob/main/cpp/ql/src/Critical/FileMayNotBeClosed.ql
分别用于寻找CWE-401
和CWE-755
,而在我看来这两类其实都属于函数调用失配的情况,即调用了malloc
没有调用free
,调用了fopen
,没有调用fclose
。这里我们通过MemoryMayNotBeFreed
进一步熟悉可达性分析模块。
直接调用or间接调用#
我们知道对一个函数的调用一般分为两种,第一种是直接调用,第二种是通过函数指针进行间接调用,为了同时考虑这两种情况,首先我们需要实现函数调用的函数如下
1 | /** |
FunctionCall
是直接调用,而VariableCall
就是间接调用了,用来处理下面这类情况
1 | /** |
直接找到变量,然后看它是不是在某处获得了函数的地址即可。
虽然
codeQL
的文档不多,但是通过看示例和它已有的注释,可以学到很多文档里没有的东西,不仅限于一些思路的写法和一些已有的API
赋值给全局or某个类的域#
1 | predicate assignedToFieldOrGlobal(StackVariable v, Expr e) { |
一般情况下,有这么两类内存分配是不一定在当前函数释放,第一类是全局的变量,它的内存释放可以在程序退出后自行释放,第二类是类内的局部变量,是由类在析构的时候释放的。
assignedToFieldOrGlobal
函数用于判断这两种情况,避免误报产生。第一种是:一个表达式,右侧是局部变量,左侧不是局部变量(也就是全局变量),这是直接赋值的情况;第三种是:在类的初始化时进行赋值;第二种是间接赋值,比如说通过参数调用函数,然后在函数内对这个参数进行了赋值。
allocCallOrIndirect#
allocCallOrIndirect
是用来找source
点的,因为我们要找MemoryMayNotBeFreed
的情况,所以我们的source
点肯定是对分配内存函数的调用
1 | predicate allocCallOrIndirect(Expr e) { |
freeCallOrIndirect#
在找内存未释放漏洞时,我们肯定要判断是不是有释放点,因此freeCallOrIndirect
就是为了找内存释放的点,同时realloc也是一种内存释放
1 | /** |
AllocVariableReachability#
1 | predicate allocationDefinition(StackVariable v, ControlFlowNode def) { |
AllocVariableReachability
是对StackVariableReachabilityWithReassignment
的继承。source
点是:存在一个表达式,它调用了分配内存的函数并赋值给了局部变量v
。sink
点可以是free或者是赋值给了全局的变量。barrier
是通过库中的函数实现的,目的是判断重新赋值的情况。
1 | /** |
AllocReachability#
AllocReachability
继承StackVariableReachabilityExt
,source点同上,sink点是需要是和变量存在在一盒函数内的return语句,barrier
是找被free
、被赋值给全局变量或者已经经过了NULL检查的情况。
1 | /** |
联合查询#
1 | from ControlFlowNode def, ReturnStmt ret |
在有了前面的铺垫后,最终的查询用自然语言描述就是:
- 从一个控制流结点到一个返回语句可达
- 且不存在一个变量保存了分配的内存的指针且最后被释放或者被赋值给全局变量等,并且申请的空间也没有作为函数返回值被返回到上层函数。
附录:MemoryMayNotBeFreed.ql#
1 | /** |