发布Trail of Bits的CodeQL查询
我们发布了一套针对Go和C语言的定制CodeQL查询集。这些查询用于发现标准CodeQL查询可能遗漏的关键问题。此次持续更新的CodeQL查询库发布,与我们的公开Semgrep规则和自动化测试手册一同,旨在与社区分享我们的技术专长。
在内部CodeQL查询的初始版本中,我们重点关注了以下问题:加密误用、不安全文件权限和字符串方法中的错误:
语言 | 查询名称 | 漏洞描述 |
---|---|---|
Go | 签名验证前消息未哈希 | 检测对(EC)DSA API的调用,其中消息未被哈希。如果消息长度超过预期哈希摘要大小,则会被静默截断。 |
Go | 文件权限缺陷 | 发现用作文件系统权限参数(FileMode)的非八进制(例如755 vs 0o755)和不支持(例如04666)的字面量。 |
Go | Trim函数误用 | 发现对string.{Trim,TrimLeft,TrimRight}的调用,其中第二个参数不是字符集(cutset),而是要修剪的连续子字符串。 |
Go | tls.Config中缺少MinVersion | 发现未为服务器显式设置tls.Config.MinVersion的情况。默认使用版本1.0,被认为不安全。此查询不标记显式设置的不安全版本。 |
C | StrNFinder | 发现调用接受字符串及其大小作为单独参数的函数(例如strncmp、strncat),但大小参数错误的情况。 |
C | 缺少空终止符 | 发现错误初始化的字符串被传递给期望以空字节终止的字符串的函数。 |
CodeQL 101
CodeQL是驱动GitHub高级安全的静态分析工具,被社区广泛用于发现漏洞。CodeQL通过将待测试代码转换为可使用类似Datalog的语言查询的数据库来操作。虽然CodeQL的核心引擎仍是专有和闭源的,但该工具提供了实现各种分析和安全查询集的开源库。
要测试我们的查询,请按照官方文档安装CodeQL CLI。准备好CodeQL CLI后,下载Trail of Bits的查询包并检查是否检测到新查询:
|
|
现在转到项目的根目录,生成一个CodeQL数据库,指定go或cpp作为编程语言:
|
|
如果生成未成功或项目有复杂的构建系统,请使用命令标志。最后,对数据库执行Trail of Bits的查询:
|
|
分析输出为静态分析结果交换格式(SARIF)。使用带有SARIF Viewer插件的Visual Studio Code打开它并分类发现的问题。或者,将结果上传到GitHub或使用--format csv
以文本形式获取结果。
Go中的(EC)DSA静默输入截断
让我们使用ECDSA签署/etc/passwd
文件。以下实现安全吗?
|
|
图1:签名生成和验证函数示例
当然不安全。问题在于将原始、未哈希且可能较长的数据传递给ecdsa.SignASN1
和ecdsa.VerifyASN1
方法,而Go的crypto/ecdsa
包(以及其他一些包)期望用于签名和验证的数据是实际数据的哈希。
这种行为意味着代码仅签署和验证文件的前32字节,因为示例中使用的P-256曲线大小为32字节。
输入数据的静默截断发生在hashToNat
方法中,该方法由ecdsa.{SignASN1,VerifyASN1}
方法内部使用:
|
|
图2:输入数据的静默截断(crypto/ecdsa/ecdsa.go)
我们在真实世界的代码库中见过此漏洞,其影响是关键性的。为解决此问题,有几种方法:
长度验证。 防止缺乏哈希问题的简单方法是验证所提供数据的长度,如go-ethereum库中所做:
|
|
图3:go-ethereum库中的验证函数(go-ethereum/crypto/secp256k1/secp256.go#126–129)
静态检测。 另一种方法是静态检测缺乏哈希。为此,我们开发了tob/go/msg-not-hashed-sig-verify
查询,该查询检测所有流向潜在有问题方法的数据流,忽略源自或经过哈希函数或切片操作的流。
我们必须解决的一个有趣问题是如何为数据流分析设置起点(源)?我们可以使用UntrustedFlowSource
类。然后分析将找到来自可能由攻击者控制的任何输入的流。然而,UntrustedFlowSource
通常需要按项目扩展才能有用,因此将其用于我们的分析会导致许多项目遗漏许多流。因此,我们的查询专注于查找最长的数据流,这些更可能指示潜在漏洞。
Go中的文件权限缺陷
你能发现以下代码中的错误吗?
|
|
图4:有错误的Go代码
文件权限通常表示为八进制整数。在我们的例子中,密钥文件最终将权限设置为0o620(或rw--w----
),允许非所有者修改文件。调用os.Chmod
方法时使用的整数字面量很可能不是开发人员想要使用的。
为了发现用作FileModes的意外整数值,我们在tob/go/file-perms-flaws
CodeQL查询中实现了一个WYSIWYG(“所见即所得”)启发式方法。“所见”是清理后的整数字面量(FileMode类型的硬编码数字)——去除下划线、去除基数前缀并左补零。“所得”是转换为八进制表示的相同整数。如果这两部分不相等,则可能存在错误。
|
|
图5:CodeQL中的WYSIWYG启发式方法
为了最小化误报,我们过滤掉常用常量(如0755或0644)但以十进制或十六进制形式表示的数字。这些已知的有效常量在isKnownValidConstant
谓词中显式定义。以下是我们如何实现此谓词:
|
|
图6:过滤常见文件权限常量的CodeQL谓词
使用非八进制数字表示不是处理文件权限时唯一可能的陷阱。另一个要注意的问题是在调用权限更改方法时使用超过九位。文件权限仅编码为前九位,其他位编码文件模式,如粘滞位或setuid。一些权限更改方法——如os.Chmod
或os.Mkdir
——忽略模式位的子集,具体取决于操作系统。tob/go/file-perms-flaws
查询也会警告此问题。
Go中的字符串修剪误用
API歧义是错误的常见来源,尤其是当有多个名称和目的相似的方法接受相同的参数集时。Go的strings.Trim
系列方法就是这种情况。考虑以下调用:
|
|
图7:歧义的Trim方法
你能说出这些调用之间的区别并确定哪个“按预期”工作吗?
根据文档,strings.TrimLeft
方法接受要移除的字符集(即一组字符),而不是前缀。因此,它删除的字符比预期的多。虽然上面的例子可能看起来无害,但例如,跨站脚本(XSS)清理函数中的错误可能具有毁灭性后果。
在寻找误用的strings.Trim{Left,Right}
调用时,棘手的部分是定义什么符合“预期”行为。为了应对这一挑战,我们开发了tob/go/trim-misuse
CodeQL查询,其中包含简单的启发式方法,基于字符集参数区分有效和可能错误的调用。如果参数包含重复字符或满足以下所有条件,我们认为Trim操作无效:
- 长度超过两个字符
- 包含至少两个连续的字母数字字符
- 不是常见的连续字符列表
虽然启发式方法看起来过于简化,但它们在我们的审计中效果足够好。在CodeQL中,上述规则实现如下。字符集是对应于strings.Trim{Left,Right}
方法调用的字符集参数的变量。
|
|
图8:Trim操作启发式方法的CodeQL实现
有趣的是,strings.Trim
方法的误用如此普遍,以至于Go开发人员正在考虑弃用和替换有问题的函数。
识别Go中缺少的最小TLS版本配置
使用静态分析工具时,了解其局限性很重要。官方的go/insecure-tls
CodeQL查询查找接受不安全(过时)TLS版本(例如SSLv3、TLSv1.1)的TLS配置。它通过将提供给配置的MinVersion和MaxVersion设置的值与已弃用版本列表进行比较来完成此任务。然而,该查询不会警告未显式设置MinVersion的配置。
为什么这应该引起关注?原因是服务器的默认MinVersion是TLSv1.0。因此,在下面的示例中,官方查询仅将server_explicit
标记为不安全配置,尽管两个服务器使用相同的MinVersion。
|
|
图9:MinVersion设置的显式和默认配置
此问题的严重性相当低,因为客户端的默认MinVersion是安全的TLSv1.2。尽管如此,我们填补了空白并开发了tob/go/missing-min-version-tls
CodeQL查询,该查询检测未显式设置MinVersion字段的tls.Config
结构。该查询跳过报告用于客户端的配置,并通过过滤掉在结构初始化后设置MinVersion的发现来限制误报。
C和C++中的字符串错误
基于我的一位Trail of Bits同事进行的富有洞察力的cstrnfinder
研究,我们开发了tob/cpp/cstrnfinder
查询。此查询旨在识别提供给期望字符串及其相应大小作为输入的函数调用的无效数字常量——例如strncmp
、strncpy
和memmove
。我们专注于检测三种错误情况:
缓冲区读取不足。 当大小参数(下例中的数字20)略小于源字符串的长度时发生:
|
|
图10:缓冲区读取不足错误示例
这里,"org/tob/test/SafeData"
字符串的长度是21字节(如果计算终止空字节,则为22)。然而,我们仅比较前20字节。因此,像"org/tob/test/SafeDatX"
这样的字符串被错误匹配。
缓冲区读取过度。 当大小参数(下例中的14)大于输入字符串的长度时出现,导致函数读取越界。
|
|
图11:缓冲区读取过度错误示例
在示例中,"Silmarillion"
字符串的长度是12字节(带空字节为13)。如果密码长于13字节并以"Silmarillion"
子字符串开头,则memcmp
函数读取pass
缓冲区之外的数据。虽然操作字符串的函数在空字节上停止读取输入缓冲区并且不会过度读取输入,但memcmp
函数操作字节并且不会在空字节上停止。
字符串连接函数的不正确使用。 如果大小参数(下例中的BUFSIZE-1)大于源字符串的长度(", Beowulf\x00"
的长度,即10字节),则大小参数可能被错误解释为目标缓冲区的大小(示例中的BUFSIZE字节),而不是输入字符串的大小。这可能指示缓冲区溢出漏洞。
|
|
图12:strncat函数误用错误示例
在上面的代码中,all_books
缓冲区最多可容纳256字节数据。如果books.txt
文件包含250个字符,则在调用strncat
函数之前缓冲区中的剩余空间为6字节。然而,我们指示函数向all_books
缓冲区的末尾添加最多255(BUFSIZE-1)字节。因此,", Beowulf"
字符串的几个字节将最终出现在分配的空间之外。我们应该做的是指示strncat
添加最多5字节(留下1字节用于终止\x00
)。
有一个类似的ID为cpp/unsafe-strncat
的内置查询,但它不适用于常量大小。
C中缺少空终止符错误
C和C++都允许开发人员使用初始化字面量构造固定大小的字符串。如果字面量的长度大于或等于分配的缓冲区大小,则字面量被截断,并且终止空字节不会附加到字符串。
|
|
图13:C字符串初始化示例
有趣的是,C编译器会警告长度超过缓冲区大小的初始化器,但不会对长度等于缓冲区大小的初始化器发出警报——即使生成的字符串都没有以空字符终止。C++编译器对两种情况都返回错误。
tob/cpp/no-null-terminator
查询使用数据流分析来查找错误初始化的字符串传递给期望以空字符终止的字符串的函数。此类函数调用会导致越界读取或写入漏洞。
CodeQL:过去、现在和未来
这将是Trail of Bits的一个持续项目,所以请期待更多!我们最有价值的开发之一是我们在自动化错误查找方面的专业知识。这个新的CodeQL存储库、Semgrep规则和自动化测试手册是帮助其他人从我们工作中受益的关键方法。请使用这些资源并向我们报告任何问题或改进!
如果您想了解更多关于我们在CodeQL上的工作,我们以多种方式使用了其功能,例如检测迭代器失效、识别未处理的错误和发现不同的表示。
如果您有兴趣为您的项目定制CodeQL查询,请与我们联系。
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News