Publishing Trail of Bits’ CodeQL queries
Paweł Płatek
December 06, 2023
c/c++, codeql, cryptography, go
我们发布了一组针对Go和C的自定义CodeQL查询。我们使用这些查询发现了标准CodeQL查询可能遗漏的关键问题。这个持续更新的CodeQL查询库的新版本,与我们的公共Semgrep规则和自动化测试手册一起,旨在与社区分享我们的技术专长。
对于我们内部CodeQL查询的初始发布,我们专注于诸如密码学误用、不安全的文件权限和字符串方法中的错误等问题:
语言 | 查询名称 | 漏洞描述 |
---|---|---|
Go | Message not hashed before signature verification | 此查询检测到调用(EC)DSA API时消息未被哈希的情况。如果消息长于预期的哈希摘要大小,它会被静默截断。 |
Go | File permission flaws | 此查询发现用作文件系统权限参数(FileMode)的非八进制(例如,755 vs 0o755)和不支持(例如,04666)的字面量。 |
Go | Trim functions misuse | 此查询发现调用string.{Trim,TrimLeft,TrimRight}时,第二个参数不是字符集(cutset),而是要修剪的连续子字符串。 |
Go | Missing MinVersion in tls.Config | 此查询发现未为服务器显式设置tls.Config.MinVersion的情况。默认使用版本1.0,这被认为是不安全的。此查询不标记显式设置的不安全版本。 |
C | CStrNFinder | 此查询发现调用接受字符串及其大小作为单独参数的函数(例如strncmp、strncat),但大小参数错误的情况。 |
C | Missing null terminator | 此查询发现错误初始化的字符串被传递给期望以null字节结尾的字符串的函数。 |
CodeQL 101
CodeQL是GitHub Advanced Security背后的静态分析工具,广泛用于社区中发现漏洞。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
以文本形式获取结果。
(EC)DSA在Go中的静默输入截断
让我们使用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}
调用时,棘手的部分是定义什么 qualifies as “预期”行为。为了解决这个挑战,我们开发了tob/go/trim-misuse
CodeQL查询,带有简单的启发式方法,基于cutset参数区分有效和可能错误的调用。如果参数包含重复字符或满足以下所有条件,我们认为Trim操作无效:
- 长于两个字符
- 包含至少两个连续的字母数字字符
- 不是常见的连续字符列表
虽然启发式方法看起来过于简化,但它们在我们的审计中工作得足够好。在CodeQL中,上述规则实现如下所示。cutset是一个变量,对应于strings.Trim{Left,Right}
方法调用的cutset参数。
|
|
图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同事进行的 insightful cstrnfinder
研究,我们开发了tob/cpp/cstrnfinder
查询。此查询旨在识别提供给期望字符串及其相应大小作为输入的函数的调用中的无效数字常量——如strncmp
、strncpy
和memmove
。我们专注于检测三种错误情况:
缓冲区欠读。 当大小参数(下面例子中的数字20)略小于源字符串的长度时发生:
|
|
图10:缓冲区欠读错误示例
这里,"org/tob/test/SafeData"
字符串的长度是21字节(22如果我们计算终止null字节)。然而,我们只比较前20字节。因此,像"org/tob/test/SafeDatX"
这样的字符串被错误匹配。
缓冲区过读。 当大小参数(下面例子中的14)大于输入字符串的长度时出现,导致函数读取越界。
|
|
图11:缓冲区过读错误示例
在例子中,"Silmarillion"
字符串的长度是12字节(13带有null字节)。如果密码长于13字节并以"Silmarillion"
子字符串开头,那么memcmp
函数读取pass
缓冲区之外的数据。虽然操作字符串的函数在null字节上停止读取输入缓冲区并且不会过读输入,但memcmp
函数操作字节并且不会在null字节上停止。
字符串连接函数的错误使用。 如果大小参数(下面例子中的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中缺失的null终止符错误
C和C++都允许开发人员使用初始化字面量构造固定大小的字符串。如果字面量的长度大于或等于分配的缓冲区大小,则字面量被截断并且终止null字节不附加到字符串。
|
|
图13:C字符串初始化示例
有趣的是,C编译器警告比缓冲区大小长的初始化器,但不针对长度等于缓冲区大小的初始化器发出警报——即使结果字符串都不是null终止的。C++编译器对两种情况都返回错误。
tob/cpp/no-null-terminator
查询使用数据流分析来查找错误初始化的字符串传递给期望null终止字符串的函数。此类函数调用导致越界读取或写入漏洞。
CodeQL:过去、现在和未来
这将是Trail of Bits的一个持续项目,所以请期待更多!我们最有价值的开发之一是我们自动化错误查找的专长。这个新的CodeQL存储库、Semgrep规则和自动化测试手册是帮助其他人从我们工作中受益的关键方法。请使用这些资源并向我们报告任何问题或改进!
如果你想了解更多关于我们在CodeQL上的工作,我们以多种方式使用了它的功能,例如检测迭代器无效、识别未处理的错误和发现 divergent representations。
如果你有兴趣为你的项目定制CodeQL查询,请联系我们。
如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容 CodeQL 101 (EC)DSA在Go中的静默输入截断 Go中的文件权限缺陷 Go中的字符串修剪误用 识别Go中缺失的最小TLS版本配置 C和C++中的字符串错误 C中缺失的null终止符错误 CodeQL:过去、现在和未来 最近的