导语:在本文中,我们将为读者深入介绍如何利用CodeQL提供的标准类来分析Python项目中的函数、语句、表达式和控制流。
前面的文章中,我们为读者简单介绍了如何利用查询控制台分析Python代码,以及用于分析Python代码的CodeQL库。在本文中,我们将为读者深入介绍如何利用CodeQL提供的标准类来分析Python项目中的函数、语句、表达式和控制流。
函数分析
在前面的文章中,我们简单的介绍了一下标准的CodeQL类Function,下面,我们开始通过示例进行深入的讲解。
查找所有名为“get …”的函数
在这个例子中,我们要找出程序中的所有“getter”函数。众所周知,对于刚刚从Java语言转换到Python语言的程序员来说,往往会习惯性地编写getter和setter方法,而不是使用属性。有时候,我们可能需要找出这些方法,那该怎么办呢?很简单,利用成员谓词Function.getName(),就能轻松找到数据库中的所有getter函数,具体代码如下所示:
import python from Function f where f.getName().matches("get%") select f, "This is a function called get..."
上面的代码并不复杂,其中where f.getName().matches(“get%”)的含义是,这里要找的函数的名称与字符串”get%”匹配;而”get%”表示以get开头的字符串,因为%在这里是一个通配符,表示其他字符。代码的运行结果如下所示:
如图所示,这里找到了大量的其名称以get开头的函数,不过,其中许多函数都不是我们要找的getter函数。
查找所有名为“get …”的方法
为了让查询返回我们真正感兴趣的内容,需要对上面的查询稍作修改。由于这里只对“方法”感兴趣,所以,我们可以通过Function.isMethod()谓词来改进我们的查询代码。
import python from Function f where f.getName().matches("get%") and f.isMethod() select f, "This is a method called get..."
上述代码的查询结果如下所示:
我们发现,这里返回的方法的名称都是以“get”开头的,但是,仍然有许多不是要找的目标方法。
查找所有名为“get…”的单行方法
我们可以进一步修改查询,使其只返回函数定义中只有一条语句方法。为此,我们可以通过统计每个方法中的代码行数来做到这一点,具体代码如下所示:
import python from Function f where f.getName().matches("get%") and f.isMethod() and count(f.getAStmt()) = 1 select f, "This function is (probably) a getter."
其中,count(f.getAStmt()) = 1表示函数的定义中只包含一条语句,其他代码非常简单,这里就不多说了。上述代码的运行结果如下所示:
如您所见,这次返回的结果明显减少了,但是,其中的许多方法仍然不是我们要找的getter方法。因此,该查询代码还需做进一步的调整,具体将在后文中详细介绍。
查找针对特定函数的调用
下面,我们要通过Call 和Name这两个类来查找对函数eval的调用,因为这个函数经常会带来安全隐患,具体代码如下所示:
import python from Call call, Name name where call.getFunc() = name and name.getId() = "eval" select call, "call to 'eval'."
其中,call.getFunc() = name and name.getId() = “eval”表示调用的函数的名称为eval。此外,Call类表示Python程序中的调用,而谓词getfunc()则用于获取被调用的表达式。而谓词getid()则用于获取名称表达式的标识符(字符串)。上述代码的运行结果如下所示:
由于Python的动态特性,该查询将返回具有eval(…) 形式的所有调用,无论它是否是对内置函数eval的调用。在后文中,我们将介绍如何使用类型推断库来查找对内置函数eval的调用。
上面,我们介绍了如何利用查找满足特定条件的函数和调用,接下来,我们开始介绍如何从语句和表达式的角度来分析Python代码。
语句与表达式分析
语句
对于Python程序来说,大部分的代码都是某种语句的形式出现的。因此,对于Python中各种类型的语句,CodeQL都提供了相应的类来加以表示。
下面是这些类的层次结构:
Stmt类 —— 语句
· Assert类 —— assert语句
· Assign类
* AssignStmt类 —— 赋值语句,如x = y
* ClassDef —— 类定义语句
* FunctionDef —— 函数定义语句
· AugAssign —— 增量赋值(augmented assignment)语句,如x += y
· Break类 —— break语句
· Continue类 —— continue语句
· Delete类 —— del语句
· ExceptStmt类 —— try语句的except部分
· Exec类 —— exec语句
· For类 —— for语句
· Global类 —— global语句
· If类 —— if语句
· ImportStar类 —— from xxx import * 语句
· Import类 —— 其他类型的import语句
· Nonlocal类 —— nonlocal语句
· Pass类 —— pass语句
· Print类 —— print语句(仅限于python 2版本)
· Raise 类 —— raise语句
· Return类 —— return语句
· Try类 —— try语句
· While类 —— while语句
· With类 —— with语句
查找多余的“global”语句
Python中的global语句用于定义全局(模块级别的)变量,否则的话,定义的就是局部变量。但是,在类或函数之外使用global语句则是没有必要的,因为在这些地方定义的变量本身就是全局的。那么,我们如何查找多余的“global”语句呢?具体代码如下所示:
import python from Global g where g.getScope() instanceof Module select g
其中,g.getScope() instanceof Module的作用是确保global语句(就是这里的Global g)的作用域为模块,而不是类或函数。上述代码的运行结果如下所示:
如您所见,在我们查找的项目中,并没有找到多余的global语句。
查找具有多余分支的“if”语句
如果if语句的一个分支中只含有pass语句,则可以进一步简化该语句,方法是反转原来的条件,并删除else子句。例如,请看下面的例子:
if cond(): pass else: do_something
对于上面的if语句来说,就符合进一步简化的条件。为了找出项目中类似的if语句,我们可以使用如下所示的查询代码:
import python from If i, StmtList l where (l = i.getBody() or l = i.getOrelse()) and forall(Stmt p | p = l.getAnItem() | p instanceof Pass) select i
其中,(l = i.getBody() or l = i.getOrelse())的作用是将StmtList l限定为if语句的分支。而forall(Stmt p | p = l.getAnItem() | p instanceof Pass)的作用则是确保l中的所有语句都是pass语句。下面展示的是上述代码返回的一个结果:
表达式
对于Python中各种类型的表达式,CodeQL都提供了相应的类来加以表示。下面是这些类的层次结构:
Expr类 —— 表达式
· Attribute类 —— 属性,如obj.attr
· BinaryExpr类 —— 二进制运算,如x+y
· BoolExpr类 —— 短路逻辑运算(Short circuit logical operations),如x and y, x or y
· Bytes类 —— 字节,如b”x”或(Python 2中的)”x”
· Call类 —— 函数调用,如f(arg)
· Compare类 —— 比较运算,如0<x<10
· Dict类 —— 字典,如{‘A’:2}
· DictComp类 —— 字典推导式,如{k: v for …}
· Ellipsis类 —— 省略号表达式,如…
· GeneratorExp类 —— 生成器表达式
· IfExp类 —— 条件表达式,如x if cond else y
· ImportExpr类 —— 表示导入模块的表达式
· ImportMember类 —— 表示从模块导入某些成员的表达式(from xxx import*语句的一部分)
· Lambda类 —— Lambda表达式
· List类 —— 列表,如[‘a’, ‘b’]
· ListComp类 —— 列表推导式,如[x for …]
· Name类 —— 对变量var的引用
· Num类 —— 数字,如3或4.2
* Floatliteral
* ImaginaryLiteral类
* IntegerLiteral类
· Repr类 —— 反引号表达
· Set类 —— 集合,如{‘a’, ‘b’}
· SetComp类 —— 集合推导式,如{x for …}
· Slice类 —— 切片;如表达式seq[0:1]中的0:1
· Starred类 —— 星号表达式,如y, *x = 1,2,3(仅限于Python 3)
· StrConst类 —— 字符串。 在Python2中,可以是字节或Unicode字符。 在Python3中,只能是Unicode字符。
· Subscript类 —— 下标运算,如seq[index]
· UnaryExpr类 —— 一元运算,如-x
· Unicode类 —— Unicode字符,如u”x”或(Python 3中的)”x”
· Yield类 —— yield表达式
· YieldFrom类 —— yield from表达式(Python 3.3+)
查找使用了“is”的整数或字符串比较运算
Python的实现通常会缓存小整数和由单个字符构成的字符串,这意味着像下面这样的比较运算通常可以正常工作,但这无法保证总是如此——所以,有时候我们可能需要查找这样的比较运算。
x is 10 x is "A"
为了查找类似上面这样的比较运算,我们可以使用如下所示的代码:
import python from Compare cmp, Expr literal where (literal instanceof StrConst or literal instanceof Num) and cmp.getOp(0) instanceof Is and cmp.getComparator(0) = literal select cmp
其中,cmp.getOp(0) instanceof Is and cmp.getComparator(0) = literal的作用是,检查第一个比较运算符是否为“is”,并且第一个操作数为字面量literal。
另外,需要注意的是,这里必须使用cmp.getOp(0)和cmp.getComparator(0),而非cmp.getOp()或cmp.getComparator()。之所以这样做,是因为比较表达式中可以有多个运算符。例如,表达式3 < x < 7 中就有两个运算符和两个操作数。使用cmp.getComparator(0)能够读取第一个操作数(在本例中为3),而cmp.getComparator(1)则是用来读取第二个操作数(这里为7)。下图展示的就是上面的查询返回的一个结果:
查找字典中的重复项
如果Python字典中存在重复的键,那么第二个键将覆盖第一个键,这几乎可以肯定是一个代码错误。为此,我们可以通过CodeQL平台提供的类来找出这些重复项,不过,这项工作可能稍微复杂一些,具体代码如下所示:
import python predicate same_key(Expr k1, Expr k2) { k1.(Num).getN() = k2.(Num).getN() or k1.(StrConst).getText() = k2.(StrConst).getText() } from Dict d, Expr k1, Expr k2 where k1 = d.getAKey() and k2 = d.getAKey() and k1 != k2 and same_key(k1, k2) select k1, "Duplicate key in dict literal"
下面是上述代码返回的一个结果,同样,这里也给出了相应的警告信息:
在上面的示例代码中,谓词same_key的作用是检查键是否具有相同的标识符。之所以将这些逻辑的单独封装成一个谓词,而不是直接将其放到查询中,是为了提高整体代码的可读性。谓词中的类型转换操作,是为了将表达式限制为指定的类型,并使谓词适用于转换后的类型。例如:
x = k1.(Num).getN()
等价于:
exists(Num num | num = k1 | x = num.getN())
只是前一种形式更加简洁,所以也更加易于理解。
查找Java风格的getter方法
让我们再次回到前面的那个例子上面:查找只包含一行代码且名称以get开头的所有方法:
import python from Function f where f.getName().matches("get%") and f.isMethod() and count(f.getAStmt()) = 1 select f, "This function is (probably) a getter."
接下来,我们将通过检查函数中的这一行代码的格式是否为return self.attr来改进上面的查询结果:
import python from Function f, Return ret, Attribute attr, Name self where f.getName().matches("get%") and f.isMethod() and ret = f.getStmt(0) and ret.getValue() = attr and attr.getObject() = self and self.getId() = "self" select f, "This function is a Java-style getter."
其中,ret = f.getStmt(0) and ret.getValue() = attr的作用是:检查方法中的第一行是否是return语句,以及返回的表达式(ret.getValue())是否是Attribute类型的表达式。请注意,等式ret.getValue() = attr意味着ret.getValue()仅限于Attribute类型,因为attr就是一个Attribute类型的值。另外,attr.getObject() = self and self.getId() = “self”的作用是,检查属性的值(即value.attr中点号左边的表达式)是否为对一个名为“self”的变量的访问。
好了,现在看看改进后的查询代码返回的结果:
类与函数的定义
由于Python是一种动态类型语言,所以,类和函数定义都是通过一些可执行语句完成的。这意味着class语句既是语句,也是包含语句的作用域。为了更加清晰地刻画这一点,类定义被分为许多个部分。在运行过程中,当执行定义类的语句时,会创建一个类对象,并将其赋给包含该类的作用域中的同名变量。实际上,这个类是通过一个代码对象创建的,而该代码对象表示的就是类主体中的源代码。为此,标准库特意将ClassDef类(用于表示class语句)定义为Assign类的子类。我们可以通过ClassDef.getDefinedClass()访问表示类主体的Class类。同时,类FunctionDef和Function的处理方式也与此类似。
下面是这些类的层次结构:
Stmt类
· Assign类
* ClassDef类
* FunctionDef类
Scope类
· Class类
· Function类
控制流分析
在分析Scope类的控制流图的时候,我们可以借助于CodeQL平台提供的两个类: ControlFlowNode 和 BasicBlock类。在进行变种分析的时候,我们经常面临这样的问题:“我们能从B点到达A点吗?”,或者“能否在不经过A点的前提下到达B点?”。为了回答这些问题,我们需要借助于类AstNode,这个类可以表示一个语法元素并对应于其源代码。有了它,我们就能让查询结果变得更加易于理解。
ControlFlowNode类
类ControlFlowNode表示的是控制流图中的节点。我们知道,抽象语法树节点与控制流节点之间存在一对多的关系。因为每个语法元素,即AstNode类,可以映射到零个、一个或多个 ControlFlowNode 类,但是每个ControlFlowNode类却仅映射到一个AstNode。
那么,为什么要把它们的关系搞得这么复杂呢?请考虑下面的 Python 代码:
try: might_raise() if cond: break finally: close_resource()
在上面的代码中,存在许多的路径。例如,调用close_resource()的路径就有三条,并且各不相同。其中,一条是常规路径,另一条是跳出循环的路径,还有一条由might_raise()引发异常所致的路径,具体可以参照下面带注释的流程图。
实际上,ControlFlowNod和AstNode这两个类最简单的用法就是查找不可达的代码。我们知道,每条通过AstNode的路径都有一个ControlFlowNode,所以,所有不可达的AstNode都没有通过ControlFlowNode的路径。 因此,所有没有对应ControlFlowNode的AstNode都是不可达的。为此,我们可以编写如下所示的代码来查找这些代码:
import python from AstNode node where not exists(node.getAFlowNode()) select node
上述代码的运行结果如下所示:
我们可以看到,这里返回了大量的结果。其中,有一些是没有控制流节点的代码,因此,它们是不可达的。同时,由于Module类也是AstNode类的一个子类,因此,上面的查询结果中也含有用C语言实现模块,以及不含有源代码的模块。所以,我们最好还是查找所有不可达的语句,具体代码如下所示:
import python from Stmt s where not exists(s.getAFlowNode()) select s
如您所见,这次返回的结果就明显减少了,但无论如何,大多数项目中总是有一些不可达的节点。
BasicBlock类
Basicblock类通常用于表示控制流节点的基本构造块。Basicblock类对于直接编写查询来说用途不大,但对于构建复杂的分析(如数据流)来说却非常有用。之所以这么说,是因为它共享了控制流节点的许多有用的属性,比如从哪里可以到达哪里,什么支配着什么,等等。但是,由于基本构造块的数量比控制流节点少,所以,查询起来会更快,更节约内存。
查找互斥的基本构造块
假设我们有如下所示的 Python 代码:
if condition(): return 0 pass
那么,我们能断定在单次执行该代码时不可能同时到达return 0语句和pass语句吗?要想让两个基本构造块互斥,就必须使其彼此不可达。为此,我们可以这样写:
import python from BasicBlock b1, BasicBlock b2 where b1 != b2 and not b1.strictlyReaches(b2) and not b2.strictlyReaches(b1) select b1, b2
然而,根据该定义,如果两个基本构造块位于不同的作用域中,那么它们就是互斥的。为了让结果更有用,我们可以要求两个基本构造块都可以从同一个函数入口点到达:
exists(Function shared, BasicBlock entry | entry.contains(shared.getEntryNode()) and entry.strictlyReaches(b1) and entry.strictlyReaches(b2) )
将这些这些条件组合起来,我们将得到如下所示的代码,其作用是查找同一函数中互斥的构造块:
import python from BasicBlock b1, BasicBlock b2 where b1 != b2 and not b1.strictlyReaches(b2) and not b2.strictlyReaches(b1) and exists(Function shared, BasicBlock entry | entry.contains(shared.getEntryNode()) and entry.strictlyReaches(b1) and entry.strictlyReaches(b2) ) select b1, b2
这通常会返回大量的结果,因为这种情况在正常的控制流中是很常见的。不过,我们可以将其作为控制流分析的示例。诸如此类的控制流分析对于数据流分析来说是非常有帮助的,我们将在后面的文章中将对此进行详细的介绍。
小结
在本文中,我们将为读者深入介绍如何利用CodeQL提供的标准类来分析项目中的函数、语句、表达式和控制流。在后面的文章中,我们将介绍如何分析数据流,以及如何进行污点跟踪。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。
参考资料:https://help.semmle.com/
转自:https://www.4hou.com/posts/BRM2
转载请注明:jinglingshu的博客 » 代码分析平台CodeQL学习手记(十)