CodeQL 数据流分析/污点分析 笔记(上篇)
欢迎大家关注公众号”天问记事簿”,以及加入天问之路知识星球,一起做技术分享,一起学习,happy hack。
前序
codeql关于数据流分析的基础文档可以在这里找到,本文中不多做叙述。
- https://codeql.github.com/docs/writing-codeql-queries/creating-path-queries/#creating-path-queries
- https://codeql.github.com/docs/codeql-language-guides/analyzing-data-flow-in-cpp/#analyzing-data-flow-in-cpp
codeql文档里对于数据流和污点的区别描述是这样的。
在标准库中,我们区分了正常数据流和污点跟追踪。
例如,如果您正在跟踪一个不安全的对象 x(可能是一些不受信任的或潜在的恶意数据),程序中的一个步骤可能会改变它的值。因此,在 y = x + 1
这样的简单计算中,正常的数据流分析会突出使用 x,而不是 y。然而,由于 y 是从 x 派生的,它会受到不受信任或“污染”信息的影响,因此它也被污染了。分析从 x 到 y 的污点流称为污点跟踪。
污点分析在数据流分析的基础之上,额外在控制流图上建立了许多边,以此实现。本文主要就笔者对该库的分析做了记录,如有错误还请指正。
使用case
污点建模
这里以rwctf中who move my block这道题涉及的nbd-server的一个漏洞为例来简述如何使用污点分析。
首先从 accept
函数开始找起,它是整个 socket 连接的起点,通过它我们可以根据交叉引用找到处理连接的函数 handle_modern_connection
:
1 | static void |
需要注意的是,默认情况下对于每个连接,server 都会 fork 一个新的子进程来单独处理。这个特性相当重要,因为我们可以利用这个特性来爆破 canary 和 PIE。
该函数会调用 negotiate
函数,并创建结构体 CLIENT
,将新连接的 fd 赋给该 client,之后后续使用 socket_read(client, addr, len)
来从 client(即我们这边)读取数据。
1 | /** |
这样,我们可以socket_read
的第二个参数(用户输入将读入到这里)作为source点,然后将看能否污点到后续调用的source_read
的第三个参数(即控制读入的长度),当然也可以做其他污点。
QL source code
使用的QL如下,由于污点分析不会对未建模的函数进行进一步污点传播,所以这里通过覆盖isAdditionalTaintStep来手动构建函数参数到函数调用的额外边,另外一个需要注意的是我使用的是semmle.code.cpp.ir.dataflow.TaintTracking
而不是文档里的semmle.code.cpp.dataflow.TaintTracking
,前者是基于IR的新API,被建议使用,也广泛应用在codeql自己的cwe case里
大概的结构是继承TaintTracking::Configuration
之后覆盖里面的方法即可。
1 | /** |
漏洞分析
最终发现的漏洞在handle_info
里的一个栈溢出
1 | static bool handle_info(CLIENT* client, uint32_t opt, GArray* servers, uint32_t cflags) { |
codeql污点分析源码分析
TaintTracking::Configuration
定义在cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/tainttracking1/TaintTrackingImpl.qll
里
它其实继承自DataFlow::Configuration
,然后扩展了isAdditionalFlowStep
,注意到这里先调用了this.isAdditionalTaintStep
,这就是我们可以继承后覆盖的代码,引入我们自己的额外边,同时它还有一个defaultAdditionalTaintStep
,这是该污点自己对数据流进行的扩展。
1 | abstract class Configuration extends DataFlow::Configuration{ |
operandToInstructionTaintStep
operandToInstructionTaintStep
用于把污点从参数流向指令返回值,这里做了许多连边处理:
- 首先是运算指令的参数连边至整个运算指令:
1 | // Taint can flow through expressions that alter the value but preserve |
这里的连边操作将算数运算、位运算、指针运算 和字段使用等指令的参数连向了整个指令。
例如如果我们污点到了len + 1
的len
,那么它将把污点从len传播到len + 1
这个AddExpr中。
- 其次是一元运算指令,这里排除了字段取地址指令
1 | // Unary instructions tend to preserve enough information in practice that we |
排除字段取地址指令的原因正如注释所说,流过 FieldAddressInstruction
可能会导致污点流从某个字段流入,从另一个不相关的字段流出。
- 此外是为其他已经建模好的函数进行污点传递,其中污点从 callInput 传播到 callOutput。
1 | modeledTaintStep(opFrom, instrTo) |
污点分析库会额外对库函数建模,对很多非常常用的函数建立额外边。这种建模是通过派生 TaintFunction
类,重写 hasTaintFlow
函数来实现的。我们可以全局搜索 TaintFunction 字符串,找到所有建模好的函数。以下是其中某个函数的建模实现:
1 | /** |
污点分析库对函数 abs
进行建模,重写 hasTaintFlow
函数,将该函数的输入参数与函数的返回值相连。这样,如果该函数的参数被污染,那么该函数的返回值也将被视为污染。
数据流分析库同样会对一些库函数进行建模,但不同的是,所建模函数的数量并没有污点分析那么多,同时连接额外边的侧重点也不一样,以 gets 函数为例,以下是它的建模实现:
1 | /** |
注意到 hasDataFlow 的实现是将传入的第一个 buf 参数与返回值连接(buf参数的值会影响到 gets 的返回值)。而 hasTaintFlow 是将 fgets 等的数据来源与 buf 连接(数据来源会污染 buf 中的数据)。
- 除此之外还涉及到ReadSideEffectInstruction/InitializeIndirectionInstruction等IR进行了额外的连边,但是笔者暂未找到合适的codeql IR文档,留待后文,但我初步推测应该和内存初始化和指针解引用等都有关系。
总结
目前看codeql的c/c++污点分析还是有局限性的,首先它并没有对c++的语法特性做适配,目前看只是字段访问的时候有额外的处理。
此外受限于符号支持,它并不能完全实现跨函数追踪,例如对于大部分标准库函数它目前都只能自己去建模,无法自动化的分析和追踪,但这不是ql的问题,是插桩的问题,这部分我在想通过静态链接能否有改善。
目前我们在自己做审计的时候,如果需要做跨函数的追踪,还是需要像我代码里一样去手动连边,这个连边可以是保守的也可以是粗放的,例如我的实现就是,如果污点传入的函数参数,就传播到函数调用。
此外根据我在做chrome QL审计的经验,可以参考Chrome Library来补一些拷贝构造函数,智能指针,虚函数调用,以及c++容器相关的边。
这些也是留待后文。