CodeQL分析python代码2-分析python代码的CodeQL库

前言

我们已经学习了QL的基础语法,已经可以对问题进行简单的查询了。但对于某一种特定的语言,以我们现在的基础还是不能对其项目代码进行清晰描述。

比如,我们想要获取python编写的flaskweb应用中可能存在SSTI漏洞的点

from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.args.get('404_url'))
    return render_template_string(template), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)

可以看到这里我们需要检测代码中是否存在request.args.get()获取的参数,并追踪该方式获得的参数404_url在后续的过程中是否经过了过滤,又或者会不会有一个等式405_test=404_url+"test code",导致405_test参数实际上也被污染了。最后看这些参数是否会回显render_template_string()到页面上。

整个过程需要考虑到参数在代码中的运行流程,所以传统的正则表达式匹配敏感字符在这种情况下就捉襟见肘了。

所以我们还需要学习codeql对python代码进行查询的相关基础知识,比如python的表达式,参数,函数等,这样才能在自己独立审计的时候举一反三。

官方教程链接:https://codeql.github.com/docs/codeql-language-guides/codeql-for-python/

当然codeql也支持其他语言的查询,链接为:
https://codeql.github.com/docs/codeql-language-guides/

分析python代码的CodeQL库

当我们需要分析python程序时,可以利用python的CodeQL库中大量的类。

关于分析python代码的CodeQL库

CodeQL平台专门提供了一个功能丰富的库,来帮助我们分析从Python项目中提取的CodeQL数据库。这个库中的类能够以面向对象的形式呈现数据库中的数据,并提供了许多抽象类和谓词来帮助我们完成常见的分析任务。这个库是通过一组QL模块(即扩展名为.qll的文件)的形式来实现的。其中,模块Python.qll的作用是导入这个库的所有核心模块,因此,我们可以在查询代码的开头部分通过下面的语句来导入完整的库

import python

分析python代码的CodeQL库包含大量类,每个类要么对应python源代码中的一种实体,要么对应于可以使用静态分析从源代码派生的实体。这些类可以分为四类:

  • 语法型 表示python源代码中实体的类
  • 控制流型 表示控制流图中实体的类
  • 数据流型 表示数据流图中实体的类
  • API图型 表示API图中实体的类

下面我们对这些类型分别介绍

用于分析语法的类

这些都是用于描述python源代码的,其中Module,ClassFunction类分别对应于python语言中的模块,类和函数。这些都被统称为Scope类,也就是作用域类。同时,每个作用域类实际上就是一个语句列表,表中的每个语句可以由STMT类的子类来进行表示。除此之外,还有一些其他的类,专门用于表示非常复杂的表达式(例如列表推导式,又称为列表解析式)的各个组成部分。总的来说,这些类都是AstNode的子类,并对应于相应的抽象语法树(AST)。同时,每棵AST树的根节点都是一个模块。

另外,符号信息通常以变量(由Variable表示)的形式附加到AST树上面

作用域 Scope

python程序通常都是由一组模块构成的,从技术上讲,模块只是一个语句列表,但我们通常认为它是由类和函数组成的。这些源代码中的*实体,即模块,类和函数,对应于三个CodeQL类,即Module,Class和Function类,它们都是Scope类的子类,其层次关系如下所示:

  • Scope
    • Module
    • Class
    • Function

(这一部分比较难以理解,打一个不太好的比方,大家可以思考如果自己是一个初代的python解释器,对于一份python代码会怎么去解析它。我们可以先读入代码,然后删除掉里面的单行注释和多行注释,因为这些对程序的结果是不影响的,然后因为python会import很多的包文件,所以我们需要把导入的包也进行读入。现在程序里面还剩下什么,python语句?变量声明?函数?还是类?事实上就是一段又一段的语句,而语句又构成了类和函数,但也会有位于类和函数之外的语句,所以语句,类,函数就可以囊括python程序在预处理之后的所有内容了。如果你要问for或者try这些在哪里?它们自然是属于上面三类的子集了)

本质上来说,无论ModuleClass还是Function都是一个语句列表,尽管Scope类具有额外的属性,例如名称等。

例如,以下查询查找函数作用域(声明它们的作用域)仍然是函数的函数。(也就是寻找函数中的函数)

import python

from Function f
where f.getScope() instanceof Function
select f

点击查询结果可见
CodeQL分析python代码2-分析python代码的CodeQL库

语句

python源代码中的语句由Stmt类表示,它大约由20个子类,表示各种语句,例如Pass,Return,For语句。语句通常由多个部分组成,其中最常见的组成部分就是表达式。CodeQL中专门提供了一个Expr类来表示表达式。例如对以下的for循环代码

for var in seq:
    pass
else:
    return 0

CodeQL中如果我们想要查询项目中的for语句,需要用到For类,该类提供了很多成员谓词,用于访问for的各个组成部分,例如:

  • getTarget() 返回变量varExpr表达式
  • getIter() 返回表示变量seqExpr表达式
  • getBody() 返回for语句列表主体
  • getStmt(0) 返回第一条语句,编号从0开始。在上面的代码中,返回的就是pass语句
  • getOrElse() 返回包含return语句的StmtList语句列表

直接这么说比较抽象,这里我们对一个flask项目进行测试,使用getTarget()

import python
from For tempFor
select tempFor.getTarget()

CodeQL分析python代码2-分析python代码的CodeQL库
点击第一个结果,即x
CodeQL分析python代码2-分析python代码的CodeQL库
可以看到就是for循环语句for x in range(len(s)-1, 1, -1):中的临时变量名

表达式

大多数语句都是由表达式组成的,Expr类是所有表达式类的父类,大概有30个类涉及调用,推导,元组,列表和算术运算。例如,我们可以使用BinaryExpr类来表示python表达式a+2

  • 成员谓词getLeft()返回表示a的表达式Expr,这里的成员谓词其实可以见名知意
  • 成员谓词getRight()返回表示2的表达式Expr

如果我们想要在项目中查找例如a+2这种左侧是简单名称而右侧是数字常量形式的表达式,我们可以使用以下查询

import python

from BinaryExpr bin
where bin.getLeft() instanceof Name and bin.getRight() instanceof Num
select bin

在我本地项目中的查询结果如下
CodeQL分析python代码2-分析python代码的CodeQL库

这种类型的可以用于污点追踪

变量

python源代码中的变量可以使用CodeQL库中的Variable类来表示,该类具有两个子类(从名字就可以看出实际上是变量作用域的不同):

  • LocalVariable用于表示函数和类级别的变量
  • GlobalVariable用于表示模块级别的变量

源代码中的其他元素

虽然程序的语义可以通过诸如ScopeStmtExpr等语法元素进行表示,但是源代码中的某些部分仍然无法通过抽象语法树来进行覆盖。例如,源代码中的注释,这里我们是使用Comment类进行表示

分析语法的一些栗子

在前面的学习中我们了解到:CodeQL平台在处理python项目的时候,会将源代码中的每个语法元素都记录在CodeQL数据库中,我们通过相应的类来查询项目中的这些语法元素。

  • 查找所有的finally语句
    我们使用Try类
import python

from Try t
select t.getFinalbody()
  • 寻找无所事事的except语句
    一个无所事事的ezcept语句,即这种
try:
    //省略
except:
    pass
//省略

也就是说除了pass语句之外不包含任何其他的语句,我们编写QL查询为

import python
 
from ExceptStmt ex
where not exists(Stmt s | s = ex.getAStmt() | not s instanceof Pass)
select ex

可能这里有一点复杂,因为用到了双重否定

exExceptStmt类的一个实例,ExceptStmt类用来表示except语句,s = ex.getAStmt()获取项目中的except语句中的内容,s的类型不能是Pass

exists(Stmt s | s = ex.getAStmt() | not s instanceof Pass)的意思就是except块中的所有语句都不是Pass类型。

最后在条件外部加上not取反,整句话的意思变成了:except块中所有语句都是Pass类型

我们也可以使用逻辑量词forall来进行表示
forall(Stmt s | s = ex.getAStmt() | s instanceof Pass)

这时候的查询语句变成了

import python

from ExceptStmt ex
where forall(Stmt s | s = ex.getAStmt() | s instanceof Pass)
select ex

查询之后的结果如图:
CodeQL分析python代码2-分析python代码的CodeQL库
点击一个进去看
CodeQL分析python代码2-分析python代码的CodeQL库

分析语法部分总结

我们介绍了使用CodeQL表示语法时最常用的标准类:ModuleClassFunctionStmt以及 Expr类,它们都是AstNode的子类

抽象语法树 Abstract syntax tree

  • AstNode
    • Module -- python模块
    • Class -- 类
    • Function -- 函数
    • Stmt -- 语句
      • Assert -- assert语句
      • Assign
        • AssignStmt -- 赋值语句 x=y
        • ClassDef -- 类定义语句
        • FunctionDef -- 函数定义语句
      • AugAssign -- 自增赋值语句 x+=y
      • Break -- break语句
      • Continue -- continue语句
      • Delete -- del语句
      • ExceptStmt -- try语句中的except部分
      • Exec -- exec语句
      • For -- for语句
      • If -- if语句
      • Pass -- pass语句
      • Print -- print语句
      • Raise -- raise语句
      • Return -- return语句
      • Try -- try语句
      • While -- while语句
      • With -- with语句
    • Expr -- 表达式
      • Attribute -- 类属性 obj.attr
      • Call -- 函数调用 f(arg)
      • IfExp -- 条件表达式 x if cond else y
      • Lambada -- lambda表达式
      • Yield -- yield表达式
      • Bytes -- 字节文字,b"x"或(在python2中)的"x"
      • Unicode -- Unicode文字,u"x"或(在python3中)的"x"
      • Num -- 数字文字 3或者4.2这种
        • IntegerLiteral 整型
        • FloatLiteral 浮点型
        • ImaginaryLiteral (这是啥类型)
      • Dict -- 字典,{'a':2}
      • Set -- 集合,{'a','b'}
      • List -- 列表,['a','b']
      • Tuple -- 元组,('a','b')
      • DictComp -- 字典推导式,{k:v for ...}
      • SetComp -- 集合推导式,{x for ...}
      • ListComp -- 列表推导式,[x for ...]
      • GenExpr -- 生成器表达式,(x for ...)
      • Subscript -- 下标操作,seq[index]
      • Name -- 对变量的引用,var
      • UnaryExpr -- 一元运算,-x
      • BinaryExpr -- 二元运算,x+y
      • Compare -- 比较操作,0<x<10
      • BoolExpr -- 短路逻辑运算,x and yx or y

变量 Variables

  • Variable -- 变量
    • LocalVariable -- 函数或者类的局部变量
    • GlobalVariable -- 全局(模块级)变量

其他

  • Comment -- 注释

控制流类

CodeQL库中的这一部分表示Scope类(即类,函数和模块)的控制流图。每个Scope类都包含一个ControlFlowNode元素构成的图。每个Scope都有一个入口点和至少一个(可能很多)的出口点。为了提高分析控制流和数据流的速度,控制流节点通常会被分组为基本构造块。

示例

如果我们想要找到最长的没有任何分支的代码序列,我们需要考虑控制流。根据定义,一个BasicBlock就是一个没有任何分支的代码序列,所有我们只需要找到最长的BasicBlock即可。

首先,我们需要引入一个谓词bb_length(),它与BasicBlock的长度相关

int bb_length(BasicBlock b) {
    result = max(int i | exists(b.getNode(i))) + 1
}

由于BasicBlock中的各个ControlFlowNode都是从0开始连续编号的,因此,BasicBlock的长度等于该基本块中最大索引+1

那么我们应该如何利用bb_length()找出最长的BasicBlock呢?显然,满足我们要求的BasicBlock基本构造快具有这样的特点:其长度与所有基本构造块中长度最长的那个相等。翻译成QL代码如下:

import python

int bb_length(BasicBlock b) {
    result = max(int i | exists(b.getNode(i)) | i) + 1
}

from BasicBlock b
where bb_length(b) = max(bb_length(_))
select b

可以注意到,这里用到了max(bb_length(_)),其中_是特殊的下划线变量,表示任意值,在这里也就是表示所有的基本构造块。

控制流类总结

CodeQL库中控制流部分的类为:

  • ControlFlowNode -- 控制流节点。AST抽象语法树节点和控制流节点之间存在一对多的关系
  • BasicBlock -- 表示一组没有分支的控制流节点

END

建了一个微信的安全交流群,欢迎添加我微信备注进群,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注

上一篇:20000+关注,开源两本硬核的原创电子书!


下一篇:神奇的两招,让超市做到人满为患,老板这一套操作打下来真绝