代码分析平台CodeQL学习手记(十一)
fanyeee 嘶吼专业版
在前面的文章中,我们为读者深入介绍了如何利用CodeQL提供的标准类来分析Python项目中的函数、语句、表达式和控制流。在本文中,我们将为读者介绍如何分析数据流,以及如何进行污点跟踪和指向分析。
概述
首先,什么是污点跟踪呢?简单来说,就是分析代码运行过程中,可能存在安全隐患或“受污染”的数据的流动情况。其次,污点跟踪有什么作用呢?作用就很多了,比如,我们可以通过污点跟踪来查明下列情况:用户控制的输入是否存在被恶意利用的隐患?危险的参数是否会传递给易受***的函数?机密或敏感的数据是否存在被泄漏的风险?除此之外,在进行其他类型的安全分析过程中,还可以通过这种方法来跟踪各种非法的、不安全或不可信的数据。
污点跟踪与基本数据流的不同之处在于,除了进行“常规的”数据流分析之外,它还考虑到了在不保留值(non-value-preserving)的情况下的数据流分析。例如,在赋值语句dir = path + "/"中,如果path被污染了,那么dir也将被污染,即使从path到path + "/"之间没有数据流动。
对于不同的语言,包括C/C++、C#、Java和JavaScript语言,CodeQL平台都为其提供了独立的、用于处理其“常规”数据流和污点跟踪的库。通过在查询中导入相应的库,我们就可以访问相应的类和谓词,以便处理这些数据流模式。在分析Python代码的时候,我们也可以使用相同的污点跟踪库来分析“常规的”数据流和污点流,同时,我们也可以通过定义额外的数据流属性来区分保留值和不保留值的情况下的处理方法。
污点跟踪与数据流分析
其实,污点跟踪库位于TaintTracking模块中。另外,用于污点跟踪或数据流分析的所有查询都具有三个显式组件(其中一个是可选的),以及一个隐式组件。这些显式组件包括:
-
一个或多个可能存在不安全数据的源点,它们由TaintTracking::Source类表示。
-
由TaintTracking::Sink类表示的一个或多个数据或污点可能流向的接收点。
- 零个或多个清洗器,由Sanitizer类表示。
在数据从源点流向接收点的过程中,如果没有遭到清洗器的拦截的话,用于污点跟踪或数据流分析的查询就会返回相应的分析结果。
这三个组件是通过TaintTracking::Configuration绑定在一起的,以便明确特定查询与哪些源点和接收点相关。
最后一个隐式组件是污点的“kind”,由TaintKind类表示。污点的类型决定了,除了执行内置的、针对“保留值”的处理之外,还执行哪些针对“非保留值”的分析步骤。例如,对于上面讲过的 dir = path + "/",当污点表示字符串的时候,则污点数据会从path流向dir,但如果污点为None的话,则不会出现这种情况。
污点跟踪的局限性
尽管污点跟踪是一种强大的技术,但值得注意的是,它严重依赖于底层的数据流图。然而,要想创建一个准确且覆盖率又高的数据流图,却是一个非常大的挑战,特别是对于像Python这样的动态语言来说。此外,调用图通常也不是很完整的,代码的可达性也很难精确测量,而某些结构,比如eval函数,由于动态性太强了,所以很难进行分析。
利用污点跟踪分析Python代码
一个简单的污点跟踪查询的基本形式如下所示:
/**
* @name ...
* @description ...
* @kind problem
*/
import semmle.python.security.TaintTracking
class MyConfiguration extends TaintTracking::Configuration {
MyConfiguration() { this = "My example configuration" }
override predicate isSource(TaintTracking::Source src) { ... }
override predicate isSink(TaintTracking::Sink sink) { ... }
/* optionally */
override predicate isExtension(Extension extension) { ... }
}
from MyConfiguration config, TaintTracking::Source src, TaintTracking::Sink sink
where config.hasFlow(src, sink)
select sink, "Alert message, including reference to $@.", src, "string describing the source"
作为一个虚构的示例,这里的查询代码想要查找从HTTP请求到名为“unsafe”的函数的数据流。当然,这些源点都是预定义好的,读者可以通过导入semmle.python.web.HttpRequest库来访问它们。此外,接收点则可以通过一个定制的TaintTracking::Sink类来进行定义。
/* Import the string taint kind needed by our custom sink */
import semmle.python.security.strings.Untrusted
/* Sources */
import semmle.python.web.HttpRequest
/* Sink */
/** A class representing any argument in a call to a function called "unsafe" */
class UnsafeSink extends TaintTracking::Sink {
UnsafeSink() {
exists(FunctionValue unsafe |
unsafe.getName() = "unsafe" and
unsafe.getACall().(CallNode).getAnArg() = this
)
}
override predicate sinks(TaintKind kind) {
kind instanceof StringKind
}
}
class HttpToUnsafeConfiguration extends TaintTracking::Configuration {
HttpToUnsafeConfiguration() {
this = "Example config finding flow from http request to 'unsafe' function"
}
override predicate isSource(TaintTracking::Source src) { src instanceof HttpRequestTaintSource }
override predicate isSink(TaintTracking::Sink sink) { sink instanceof UnsafeSink }
}
from HttpToUnsafeConfiguration config, TaintTracking::Source src, TaintTracking::Sink sink
where config.hasFlow(src, sink)
select sink, "This argument to 'unsafe' depends on $@.", src, "a user-provided value"
实现路径查询
尽管上面的污点查询查询能够告诉我们哪些源点流向哪些接收点,但它并没有告诉我们具体的路径。为此,我们需要编写一个查询代码来找出这些路径。
实际上,只要将@kind problem改为@kind path-problem,添加一条import语句,并将查询代码中的相关子句的格式稍作修改,我们就可以将标准的污点跟踪查询转换为路径查询。其中,需要添加的import语句如下所示:
import semmle.python.security.Paths
然后,将相关格式变为:
from Configuration config, TaintedPathSource src, TaintedPathSink sink
where config.hasFlowPath(src, sink)
select sink.getSink(), src, sink, "Alert message, including reference to $@.", src.getSource(), "string describing the source"
修改后的完整代码如下所示:
/**
* ...
* @kind path-problem
* ...
*/
/* This computes the paths */
import semmle.python.security.Paths
/* Expose the string taint kinds needed by our custom sink */
import semmle.python.security.strings.Untrusted
/* Sources */
import semmle.python.web.HttpRequest
/* Sink */
/** A class representing any argument in a call to a function called "unsafe" */
class UnsafeSink extends TaintTracking::Sink {
UnsafeSink() {
exists(FunctionValue unsafe |
unsafe.getName() = "unsafe" and
unsafe.getACall().(CallNode).getAnArg() = this
)
}
override predicate sinks(TaintKind kind) {
kind instanceof StringKind
}
}
class HttpToUnsafeConfiguration extends TaintTracking::Configuration {
HttpToUnsafeConfiguration() {
this = "Example config finding flow from http request to 'unsafe' function"
}
override predicate isSource(TaintTracking::Source src) { src instanceof HttpRequestTaintSource }
override predicate isSink(TaintTracking::Sink sink) { sink instanceof UnsafeSink }
}
from HttpToUnsafeConfiguration config, TaintedPathSource src, TaintedPathSink sink
where config.hasFlowPath(src, sink)
select sink.getSink(), src, sink, "This argument to 'unsafe' depends on $@.", src.getSource(), "a user-provided value"
自定义污点类型
在上面的示例中,我们是假设存在合适的TaintKind的,但是,有时我们还需要对其他对象的流动情况进行分析,例如数据库连接或None。
为此,我们可以使用类TaintTracking::Source和TaintTracking::Sink的谓词来确定源点和接收点模型的污点类型。
abstract class Source {
abstract predicate isSourceOf(TaintKind kind);
...
}
abstract class Sink {
abstract predicate sinks(TaintKind taint);
...
}
实际上,TaintKind本身只是一个字符串(一个QL字符串,而不是表示Python字符串的CodeQL实体),它提供了扩展流程的方法,并允许污点的种类随路径改变。此外,TaintKind类还提供了许多谓词,可以用来对流程进行修改。当然,这个最简单的TaintKind并没有覆盖任何谓词,这意味着它只作为不透明的数据流动。下面给出了一个查找硬编码凭据的查询,它不仅定义了污点类型类:HardcodedValue,同时还自定义了表示源点和接收点的类,具体代码如下所示:
class HardcodedValue extends TaintKind {
HardcodedValue() {
this = "hard coded value"
}
}
class HardcodedValueSource extends TaintTracking::Source {
...
override predicate isSourceOf(TaintKind kind) {
kind instanceof HardcodedValue
}
}
class CredentialSink extends TaintTracking::Sink {
...
override predicate sinks(TaintKind kind) {
kind instanceof HardcodedValue
}
}
完整的代码如下所示:
/**
* @name Hard-coded credentials
* @description Credentials are hard coded in the source code of the application.
* @kind problem
* @problem.severity error
* @precision medium
* @id py/hardcoded-credentials
* @tags security
* external/cwe/cwe-259
* external/cwe/cwe-321
* external/cwe/cwe-798
*/
import python
import semmle.python.security.TaintTracking
import semmle.python.filters.Tests
class HardcodedValue extends TaintKind {
HardcodedValue() {
this = "hard coded value"
}
}
bindingset[char, fraction]
predicate fewer_characters_than(StrConst str, string char, float fraction) {
exists(string text, int chars |
text = str.getText() and
chars = count(int i | text.charAt(i) = char) |
/* Allow one character */
chars = 1 or
chars < text.length() * fraction
)
}
predicate possible_reflective_name(string name) {
exists(any(ModuleObject m).attr(name))
or
exists(any(ClassObject c).lookupAttribute(name))
or
any(ClassObject c).getName() = name
or
exists(ModuleObject::named(name))
or
exists(Object::builtin(name))
}
int char_count(StrConst str) {
result = count(string c | c = str.getText().charAt(_))
}
predicate capitalized_word(StrConst str) {
str.getText().regexpMatch("[A-Z][a-z]+")
}
predicate format_string(StrConst str) {
str.getText().matches("%{%}%")
}
predicate maybeCredential(ControlFlowNode f) {
/* A string that is not too short and unlikely to be text or an identifier. */
exists(StrConst str |
str = f.getNode() |
/* At least 10 characters */
str.getText().length() > 9 and
/* Not too much whitespace */
fewer_characters_than(str, " ", 0.05) and
/* or underscores */
fewer_characters_than(str, "_", 0.2) and
/* Not too repetitive */
exists(int chars |
chars = char_count(str) |
chars > 15 or
chars*3 > str.getText().length()*2
) and
not possible_reflective_name(str.getText()) and
not capitalized_word(str) and
not format_string(str)
)
or
/* Or, an integer with over 32 bits */
exists(IntegerLiteral lit |
f.getNode() = lit
|
not exists(lit.getValue()) and
/* Not a set of flags or round number */
not lit.getN().matches("%00%")
)
}
class HardcodedValueSource extends TaintSource {
HardcodedValueSource() {
maybeCredential(this)
}
override predicate isSourceOf(TaintKind kind) {
kind instanceof HardcodedValue
}
}
class CredentialSink extends TaintSink {
CredentialSink() {
exists(string name |
name.regexpMatch(getACredentialRegex()) and
not name.suffix(name.length()-4) = "file"
|
any(FunctionObject func).getNamedArgumentForCall(_, name) = this
or
exists(Keyword k |
k.getArg() = name and k.getValue().getAFlowNode() = this
)
or
exists(CompareNode cmp, NameNode n |
n.getId() = name
|
cmp.operands(this, any(Eq eq), n)
or
cmp.operands(n, any(Eq eq), this)
)
)
}
override predicate sinks(TaintKind kind) {
kind instanceof HardcodedValue
}
}
/**
* Gets a regular expression for matching names of locations (variables, parameters, keys) that
* indicate the value being held is a credential.
*/
private string getACredentialRegex() {
result = "(?i).*pass(wd|word|code|phrase)(?!.*question).*" or
result = "(?i).*(puid|username|userid).*" or
result = "(?i).*(cert)(?!.*(format|name)).*"
}
class HardcodedCredentialsConfiguration extends TaintTracking::Configuration {
HardcodedCredentialsConfiguration() { this = "Hardcoded coredentials configuration" }
override predicate isSource(TaintTracking::Source source) { source instanceof HardcodedValueSource }
override predicate isSink(TaintTracking::Sink sink) {
sink instanceof CredentialSink
}
}
from HardcodedCredentialsConfiguration config, TaintSource src, TaintSink sink
where config.hasFlow(src, sink) and
not any(TestScope test).contains(src.(ControlFlowNode).getNode())
select sink, "Use of hardcoded credentials from $@.", src, src.toString()
下面展示的是上述查询返回的一个结果:
指向分析与类型推断
接下来,我们开始为读者介绍如何使用CodeQL库提供的标准类进行类型推断。
Value类
Value类及其子类FunctionValue、ClassValue和ModuleValue用于表示运行过程中表达式的可能取值。下面给出这几个类的层次结构:
Value
· ClassValue
· FunctionValue
· ModuleValue
指向分析和类型推断
指向分析,有时也称为指针分析,用于确定表达式在运行时可能“指向”哪些对象。类型推断允许我们在运行时推断表达式的类型(类)。
谓词ControlFlowNode.pointsTo(...) 可用于展示控制流节点在运行时可能“指向”的对象。ControlFlowNode.pointsTo(...)具有三种调用方式,具体如下所示:
predicate pointsTo(Value object)
predicate pointsTo(Value object, ControlFlowNode origin)
predicate pointsTo(Context context, Value object, ControlFlowNode origin)
其中,object是控制流节点引用的对象,origin是对象的来源,这对于显示有意义的结果是非常有用的。
第三种形式包括控制流节点引用对象的上下文。这种形式通常可以忽略。
ControlFlowNode.pointsTo() 无法找到控制流节点可能指向的所有对象,因为它不可能准确地找到所有可能的值。实际上,我们更喜欢准确率(也就是结果中没有不正确的值)而不是召回率(也就是尽可能找到更多的值)。 我们这样做是为了使基于指向分析的查询具有较少的假阳性结果,从而使其变得更加有用。
对于涉及多个阶段的复杂数据流分析,可以使用准确率更高的ControlFlowNode版本,但是对于简单的用例,可以选择基于Expr类的版本,因为它用起来更加简单一些。为了方便起见,Expr类也提供了三个一模一样的谓词。下面是Expr.pointsTo(...) 的三种常见调用形式:
predicate pointsTo(Value object)
predicate pointsTo(Value object, AstNode origin)
predicate pointsTo(Context context, Value object, AstNode origin)
指向分析应用示例
在本例中,我们将利用指向分析来构建更加复杂的查询。实际上,该查询已经被包含在标准查询集中了。
我们希望在try语句中找到顺序错误的except语句块。例如,如果更通用的异常处理代码位于更具体的异常处理代码之前的话,那么第二个except语句块通常永远都没有机会被执行。
首先,我们可以编写一个查询来查找具有特定顺序的两个except语句块的try语句,具体代码如下所示:
import python
from Try t, ExceptStmt ex1, ExceptStmt ex2
where
exists(int i, int j |
ex1 = t.getHandler(i) and ex2 = t.getHandler(j) and i < j
)
select t, ex1, ex2
这里,ex1和ex2都是try语句t中的异常处理代码。通过使用索引i和j,我们还可以确保ex1位于ex2之前。下面展示的是上述代码返回的一个结果:
如果只想返回ex1比ex2更通用的结果,则需要对其结果进行相应的过滤。如果一个except语句块处理的类是另一个的超类,那么它就比另一个except语句块更通用,具体代码如下所示:
exists(ClassValue cls1, ClassValue cls2 |
ex1.getType().pointsTo(cls1) and
ex2.getType().pointsTo(cls2) |
not cls1 = cls2 and
cls1 = cls2.getASuperType()
)
其中,ex1.getType().pointsTo(cls1)的作用是,确保cls1是except语句块要处理的ClassValue。好了,将这些组合在一起,我们就能找到更通用的except语句块位于更专用的except语句块之前的try语句了,具体代码如下所示:
import python
from Try t, ExceptStmt ex1, ExceptStmt ex2
where
exists(int i, int j |
ex1 = t.getHandler(i) and ex2 = t.getHandler(j) and i < j
)
and
exists(ClassValue cls1, ClassValue cls2 |
ex1.getType().pointsTo(cls1) and
ex2.getType().pointsTo(cls2) |
not cls1 = cls2 and
cls1 = cls2.getASuperType()
)
select t, ex1, ex2
这次,它只返回了一个结果,具体如下所示:
类型推断应用示例
在下面的示例中,我们将使用类型推断来确定某个对象何时用作for语句中的一个序列,但是该对象却可能不是“可迭代”的。
首先,我们需要找出在for循环中使用的对象:
from For loop, Value iter
where loop.getIter().pointsTo(iter)
select loop, iter
然后,我们需要确定对象iter是否是可迭代的。为此,我们可以考察ClassValue,看看它是否具有iter属性。下面是查找用作循环迭代器的不可迭代对象的具体代码:
import python
from For loop, Value iter, ClassValue cls
where loop.getIter().getAFlowNode().pointsTo(iter) and
cls = iter.getClass() and
not exists(cls.lookup("__iter__"))
select loop, cls
上述代码的返回结果如下所示:
可以看出,许多项目都使用了不可迭代的对象作为循环迭代器。并且,许多结果都将cls视作NoneType。如果能够显示这些None值可能的来源的话,将会对我们很有帮助。为此,我们使用pointsTo的最后一个字段,具体代码如下所示:
import python
from For loop, Value iter, ClassValue cls, AstNode origin
where loop.getIter().pointsTo(iter, origin) and
cls = iter.getClass() and
not cls.hasAttribute("__iter__")
select loop, cls, origin
下面是上述代码的返回结果,我们可以看到,它在第3列显示了None的来源,具体如下所示:
利用call-graph查找所有的调用
我们知道,Value类提供了一个getACall()方法,可以用来查找对特定函数的调用(包括内建函数)。
如果我们希望将可调用对象限制为实际函数,可以借助于FunctionValue类,它是Value的一个子类,对应于Python中的函数对象;类似的,ClassValue类则对应于Python中的类对象。
现在,我们将再次使用之前曾经介绍过的一个例子:查找对于eval函数的调用,之前的查询代码如下所示:
import python
from Call call, Name name
where call.getFunc() = name and name.getId() = "eval"
select call, "call to 'eval'."
从其返回结果来看,许多项目都是用了eval函数,具体如下所示:
不过,上述查询代码具有两个问题:
-
它假设对名为“eval”的东西的调用都是对内置eval函数的调用,这可能会导致一些假阳性结果。
- 它假设eval不能被任何其他名称引用,这可能会导致一些假阴性结果。
实际上,如果借助于调用图分析,我们可以得到更准确的结果。首先,我们可以通过谓词Value::named来准确地识别eval函数,具体代码如下所示:
import python
from Value eval
where eval = Value::named("eval")
select eval
然后,我们可以通过Value.getACall()来识别对eval函数的调用,具体代码如下所示:
import python
from ControlFlowNode call, Value eval
where eval = Value::named("eval") and
call = eval.getACall()
select call, "call to 'eval'."
经过修改后,我们就可以准确地识别对内置eval函数的调用了,即使在使用别名引用它们的情况下也是如此。这样的话,调用其他eval函数的所有假阳性结果就都被消除了,因此,它的返回结果比以前更少了。
小结
在本文中,我们为读者介绍了如何分析数据流,以及如何进行污点跟踪和指向分析。在后面的文章中,我们将继续为读者介绍更多更有趣的内容。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。