代码分析平台CodeQL学习手记(十) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

代码分析平台CodeQL学习手记(十)

fanyeee 技术 2020-01-16 10:05:00
963325
收藏

导语:在本文中,我们将为读者深入介绍如何利用CodeQL提供的标准类来分析Python项目中的函数、语句、表达式和控制流。

代码分析平台CodeQL入门(一)

代码分析平台CodeQL学习手记(二)

代码分析平台CodeQL学习手记(三)

代码分析平台CodeQL学习手记(四)

代码分析平台CodeQL学习手记(五)

代码分析平台CodeQL学习手记(六)

代码分析平台CodeQL学习手记(七)

代码分析平台CodeQL学习手记(八)

代码分析平台CodeQL学习手记(九)

前面的文章中,我们为读者简单介绍了如何利用查询控制台分析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开头的字符串,因为%在这里是一个通配符,表示其他字符。代码的运行结果如下所示:

1.png

如图所示,这里找到了大量的其名称以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..."

上述代码的查询结果如下所示:

3.png

我们发现,这里返回的方法的名称都是以“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表示函数的定义中只包含一条语句,其他代码非常简单,这里就不多说了。上述代码的运行结果如下所示:

2.png

如您所见,这次返回的结果明显减少了,但是,其中的许多方法仍然不是我们要找的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()则用于获取名称表达式的标识符(字符串)。上述代码的运行结果如下所示:

4.png

由于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)的作用域为模块,而不是类或函数。上述代码的运行结果如下所示:

5.png

如您所见,在我们查找的项目中,并没有找到多余的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语句。下面展示的是上述代码返回的一个结果:

6.png

表达式

对于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)。下图展示的就是上面的查询返回的一个结果:

7.png

查找字典中的重复项

如果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"

下面是上述代码返回的一个结果,同样,这里也给出了相应的警告信息:

8.png

在上面的示例代码中,谓词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”的变量的访问。

好了,现在看看改进后的查询代码返回的结果:

8.png

类与函数的定义

由于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()引发异常所致的路径,具体可以参照下面带注释的流程图。

9.png

实际上,ControlFlowNod和AstNode这两个类最简单的用法就是查找不可达的代码。我们知道,每条通过AstNode的路径都有一个ControlFlowNode,所以,所有不可达的AstNode都没有通过ControlFlowNode的路径。 因此,所有没有对应ControlFlowNode的AstNode都是不可达的。为此,我们可以编写如下所示的代码来查找这些代码:

import python
 
from AstNode node
where not exists(node.getAFlowNode())
select node

上述代码的运行结果如下所示:

10.png

我们可以看到,这里返回了大量的结果。其中,有一些是没有控制流节点的代码,因此,它们是不可达的。同时,由于Module类也是AstNode类的一个子类,因此,上面的查询结果中也含有用C语言实现模块,以及不含有源代码的模块。所以,我们最好还是查找所有不可达的语句,具体代码如下所示:

import python
 
from Stmt s
where not exists(s.getAFlowNode())
select s

11.png

如您所见,这次返回的结果就明显减少了,但无论如何,大多数项目中总是有一些不可达的节点。

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

12.png

这通常会返回大量的结果,因为这种情况在正常的控制流中是很常见的。不过,我们可以将其作为控制流分析的示例。诸如此类的控制流分析对于数据流分析来说是非常有帮助的,我们将在后面的文章中将对此进行详细的介绍。

小结

在本文中,我们将为读者深入介绍如何利用CodeQL提供的标准类来分析项目中的函数、语句、表达式和控制流。在后面的文章中,我们将介绍如何分析数据流,以及如何进行污点跟踪。

备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。

参考资料:https://help.semmle.com/

如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务