使用Codeql进行污点跟踪

Oct 26, 2022    #codeql   #%E6%B1%A1%E7%82%B9%E8%B7%9F%E8%B8%AA  

Codeql 简介

CodeQL 是开发人员用来自动执行安全检查以及安全研究人员用来执行变体分析的分析引擎。

在 CodeQL 中,代码被视为数据。安全漏洞、错误和其他错误被建模为可以从代码中提取的数据库执行的查询。可以运行由 GitHub 研究人员和社区贡献者编写的标准 CodeQL 查询,或者编写自己的查询以用于自定义分析。发现潜在错误的查询直接在源文件中突出显示结果。

QL语言简介

QL 是一种声明式的、面向对象的查询语言,经过优化可以有效分析分层数据结构,特别是表示软件组件的数据库。

QL 的语法类似于 SQL,但 QL 的语义基于 Datalog,一种经常用作查询语言的声明式逻辑编程语言。这使得 QL 主要是一种逻辑语言,QL 中的所有操作都是逻辑操作。此外,QL 继承了 Datalog 的递归谓词,并增加了对聚合的支持,即使是复杂的查询也变得简洁明。例如,考虑一个包含人的父子关系的数据库。如果我们想找到一个人的后代数量,通常我们会:

  1. 查找给定人的后代,即孩子或孩子的后代。
  2. 计算使用上一步找到的后代数。

当你用 QL 编写这个过程时,它非常类似于上面的结构。请注意,我们使用递归来查找给定人的所有后代,并使用聚合来计算后代的数量。由于语言的声明性质,在不添加任何过程细节的情况下将这些步骤转换为最终查询是可能的。QL 代码看起来像这样:

Person getADescendant(Person p) {
  result = p.getAChild() or
  result = getADescendant(p.getAChild())
}

int getNumberOfDescendants(Person p) {
  result = count(getADescendant(p))
}

以下是通用编程语言和 QL 之间在概念和功能上的一些显著差异:

QL语言的具体定义请参阅官方文档

基本的查询结构

使用 CodeQL 编写的查询具有文件扩展名 “.ql”,并包含一个 select 子句。许多现有查询包括额外的可选信息,并具有以下结构:

/**
 *
 * Query metadata
 *
 */

import /* ... CodeQL libraries or modules ... */

/* ... Optional, define CodeQL classes and predicates ... */

from /* ... variable declarations ... */
where /* ... logical formula ... */
select /* ... expressions ... */

污点跟踪的基本概念

以下以 python 代码示例,介绍污点跟踪的基本概念。

Source

可以将 “source” 理解为我们想要跟踪其处理流程的数据在代码中首次出现的位置。例如,source 可以是用户输入或硬编码字符串(匹配特定字符串的形式),有时将 source 称为“污染”数据(例如 TaintTracking::Configuration 允许我们指定和自定义 source、sink 和流配置的其他几个部分)。

正则表达式注入查询中的 source

给定以下代码段:

@app.route("/direct")
def direct():

    unsafe_pattern = request.args["pattern"]
    re.search(unsafe_pattern, "foo")

由于我们正在寻找的漏洞发生在用户输入流入正则表达式操作(正则表达式注入 )的第一个参数时,因此此处的 source 是 request.args["pattern"]。即使有其他方法来模拟此漏洞,数据流的 source 也保持不变,因为 request.args["pattern"] 是用户输入在代码中第一次出现的位置。

RemoteFlowSource

由于大多数与安全相关的查询的重点是,检查用户输入是否流入代码的特定部分(例如,函数的参数),因此 CodeQL 引入了一种结构为开发人员编译每个用户输入,开发者可以直接引入 RemoteFlowSource:

import python
import semmle.python.dataflow.new.RemoteFlowSources

from RemoteFlowSource rfs // create a 'rfs' variable of type RemoteFlowSource
select rfs // return all of its appearances

Sink

与 source 相反,“sink” 是 source 必须到达的能够导致漏洞的最后一个代码位置。

如下这段代码:

@app.route("/demo")
def demo():

    cmd = request.args["pattern"]
    result = os.popen(cmd).read() # [1]
    return f"{cmd} has returned {result}" # [2]

这段代码中 source request.args["pattern"] 最后一次出现的位置是 [2],但是根据以上定义,此查询中的实际 sink 位置是 [1],[1]是 source 到达的会导致漏洞的最后位置。

正则表达式注入查询中的 sink

给定以下代码段:

@app.route("/direct")
def direct():

    unsafe_pattern = request.args["pattern"]
    re.search(unsafe_pattern, "foo")

本例中的 sink 是 re.search 的第一个参数 unsafe_pattern

污点跟踪配置谓词

这个谓词是 “额外的(非必需)”,允许为污点跟踪配置指定一些细节。

isAdditionalTaintStep

isAdditionalTaintStep 让我们指定额外的“跳转”来“绕过”已知函数后,污染流可能会继续。如果指定此谓词,一旦 source 流动结束,CodeQL 应用指定的步骤继续寻找污染流。例如:

@app.route("/lxml.etree.parse")
def lxml_parse():

    xml_content = request.args['xml_content']
    xml_content = StringIO(xml_content)

    return lxml.etree.parse(xml_content).text

CodeQL 污点跟踪将看到 request.args['xml_content'] 流向 StringIO 并且会停止,因为下一步将是 lxml.etree.parse(here),但这里的 here 将是StringIO(request.args['xml_content']) ,而不是 request.args['xml_content']。换句话说,lxml.etree.parse 的第一个参数是 StringIO(即使代码是易受攻击的)的结果。发生这种情况是因为,如果受污染的数据流入更改其内容的函数,CodeQL 可能会停止污染流分析。在这种情况下,StringIO 返回一个文件的文件名,其内容是提供的参数。

要使 Codeql 在此种情况下继续工作,需要指定一个附加的污染步骤:StringIO 第一个参数是 nodeFromStringIO整个调用是 nodeTo

isAdditionalTaintStep 污点跟踪配置中的谓词覆盖:

override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
  exists(DataFlow::CallCfgNode ioCalls |
    ioCalls = API::moduleImport("io").getMember(["StringIO", "BytesIO"]).getACall() and
    nodeFrom = ioCalls.getArg(0) and
    nodeTo = ioCalls
  )
}

Sanitizer

Sanitizers 与 AdditionalTaintStep 相反,让我们指定不希望 CodeQL 污染流通过的函数或行为。如果指定,每次污染流前进一步,它将检查此特定步骤/行为是否指定 Sanitizers(如果是,污染流将停止)。

给定以下片段:

@app.route("/direct")
def direct():

    unsafe_pattern = request.args['pattern']
    safe_pattern = re.escape(unsafe_pattern)
    re.search(safe_pattern, "")

此示例中 CodeQL 将 re.escape 视为不会消除污染的函数(它一直被污染,因此流不会停止),我们应该将其指定为具有净化行为。

指定 re.escape 的第一个参数为 isSanitizernode 参数,如果 CodeQL 的污染流在此位置,它将停止。

override predicate isSanitizer(DataFlow::Node sanitizer) {
    sanitizer = API::moduleImport("re").getMember("escape").getACall().getArg(0)
}

此外,我们可以使用 isSanitizerGuard 来指定希望污染流停止的另一种情况。例如,StringConstCompare: (根据 qldoc: 通过与常量字符串进行比较,进行未知 node 的验证:)

override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
    guard instanceof StringConstCompare
}

污点跟踪配置的基本示例

/**
 * A taint-tracking configuration for detecting code injections.
 */
class CodeInjectionFlowConfig extends TaintTracking::Configuration {
  CodeInjectionFlowConfig() { this = "CodeInjectionFlowConfig" }

  override predicate isSource(DataFlow::Node source) { 
    source instanceof RemoteFlowSource 
  }

  override predicate isSink(DataFlow::Node sink) {
    sink = API::builtin("eval").getACall().getArg(0)
  }
}

此污点跟踪配置将检测流向任何 eval 调用的第一个参数的所有 RemoteFlowSource

让我们针对以下代码段尝试一下:

from flask import Flask, request

app = Flask(__name__)

@app.route("/flow1")
def flow1():
    code = request.args["code"]
    eval(code)


@app.route("/flow2")
def flow2():
    email = request.args["email"]
    eval("./send_email {email}".format(email=email))


def flow3_extra(text):
    return text.split("\n")

@app.route("/flow3")
def flow3():
    text = request.args["text"]
    eval(flow3_extra(text))


@app.route("/flow4")
def flow4():
    text = request.args["text"]
    tixt = text
    toxt = flow3_extra(tixt)
    tuxt = toxt
    eval(tuxt)


@app.route("/flow1_good")
def flow1_good():
    code = request.args["code"]
    if code == "print('Hello, Wo... CodeQL!')":
        eval(code)

在这个片段中,我们会测试:

我们的查询将如下所示:

/*
 * @kind path-problem
 */
import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
import DataFlow::PathGraph

/**
 * A taint-tracking configuration for detecting code injections.
 */
class CodeInjectionFlowConfig extends TaintTracking::Configuration {
  CodeInjectionFlowConfig() { this = "CodeInjectionFlowConfig" }

  override predicate isSource(DataFlow::Node source) { 
    source instanceof RemoteFlowSource 
  }

  override predicate isSink(DataFlow::Node sink) {
    sink= API::builtin("eval").getACall().getArg(0)
  }
}

from CodeInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where
    config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ eval argument comes from a $@",
    sink.getNode(), "This", source.getNode(), "user-provided value"

基本上,我们告诉 Codeql 当污点跟踪配置检查出 source 是 RemoteFlowSource 且 sink 是 eval 调用的第一个参数时,给出每个 source 和 sink 。由于使用了 DataFlow::PathNode@kind path-problem ,结果将以污染流可以被轻松跟踪的方式显示。

正如看到的,除了 flow1_good 函数之外的所有函数都是漏洞,即使 flow1_good 也被标记了出来,但这是一个假阳性的结果。正如 Sanitizer 所述,可以添加一个像 StringConstCompare 这样的 Sanitizers Guard 来避免 Codeql 污染流流过 “==" 比较运算。

/*
 * @kind path-problem
 */
import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.BarrierGuards
import DataFlow::PathGraph

/**
 * A taint-tracking configuration for detecting code injections.
 */
class CodeInjectionFlowConfig extends TaintTracking::Configuration {
  CodeInjectionFlowConfig() { this = "CodeInjectionFlowConfig" }

  override predicate isSource(DataFlow::Node source) { 
    source instanceof RemoteFlowSource 
  }

  override predicate isSink(DataFlow::Node sink) {
    sink= API::builtin("eval").getACall().getArg(0)
  }

  override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
    guard instanceof StringConstCompare
  }
}

from CodeInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where
    config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ eval argument comes from a $@",
    sink.getNode(), "This", source.getNode(), "user-provided value"

以上仅为使用 Codeql 进行污点跟踪挖掘漏洞的简单示例,实际中的漏洞查询要比以上示例更复杂。