使用CodeQL捕捉OpenSSL误用 - Trail of Bits博客
我创建了五个CodeQL查询,用于捕捉OpenSSL libcrypto API中潜在的严重错误。这是一个广泛使用但常常严苛的API,误用可能导致内存泄漏、认证绕过和其他密码学实现中的微妙问题。这些查询——我在实习期间与导师Fredrik Dahlgren和Filipe Casal共同开发——通过确保正确的密钥处理、熵初始化和检查bignums是否被清除来帮助防止误用。
要在自己的代码库上运行我们的查询,首先必须使用以下命令从存储库下载它们:
|
|
要使用CodeQL CLI在预生成的C或C++数据库上运行查询,只需将查询包的名称传递给工具,如下所示:
|
|
现在,有了这些,让我们深入探讨我在实习期间编写的实际查询。
哦不,别动我的密钥!
在使用OpenSSL初始化密码时使用过短的密钥可能导致严重问题:OpenSSL API仍会接受此密钥为有效,并在密码初始化时简单地越界读取,可能用弱密钥初始化密码,使数据易受攻击。因此,我们决定创建一个查询,通过检查密钥大小与所用算法来测试过短的密钥。幸运的是,OpenSSL使用了一个命名方案,使得实现此查询变得容易。(稍后详述。)
以下是函数EVP_EncryptInit_ex的定义,用于初始化新的对称密码。
注意函数如何将密钥作为第四个参数。考虑到这一点,我们可以使用CodeQL通过数据流分析在CodeQL中定义Key类型。如果有数据流从变量流入EVP_EncryptInit_ex的密钥参数,该变量很可能代表一个密钥(或至少被用作一个)。因此,我们可以使用CodeQL定义密钥如下:
这里,我们使用数据流确保密钥流入对EVP_EncryptInit_ex调用的密钥参数。这之所以有效,是因为包含转换的语句仅在init满足CodeQL的EVP_EncryptInit_ex定义(即,如果它代表对名为EVP_EncryptInit_ex的函数的调用)时评估为真。对getKey()的调用简单地返回在EVP_EncryptInit_ex调用中密钥参数的位置。
接下来,我们需要能够使用CodeQL评估密钥的大小。为了检查给定密钥是否有正确的大小,我们需要知道两件事:密钥的大小和密钥传递给的密码的密钥大小。获取密钥的大小很简单,因为Codeql有一个getSize()谓词,返回类型的大小(以字节为单位)。对getUnderlyingType()的调用用于解析typedef并获取密钥的基础类型。
现在,我们需要确定密钥的大小应该是什么。这显然取决于使用的密码。然而,CodeQL不知道密码是什么。在OpenSSL中,高级EVP API暴露的每个密码都是EVP_CIPHER类型的一个实例,并且每个密码都使用API中的特定函数进行初始化。例如,如果我们想在CBC模式下使用AES-256,我们将从EVP_aes_256_cbc()返回的EVP_CIPHER实例传递给EVP_EncryptInit_ex。由于API名称包含密码的名称,我们可以使用CodeQL中的getName()和matches()谓词来比较函数调用的名称与密码名称中的模式。
由于密码由函数调用(的返回值)给出,并且我们想匹配目标函数的名称,我们需要使用getTarget()来获取调用的基础目标。为了约束密码的密钥大小,我们为密钥大小添加一个字段,并在构造函数中约束该字段的值。
接下来,我们需要检查传递给密码的密钥是否等于预期大小。然而,我们必须小心,并检查我们比较的密码是否实际与密钥一起使用,而不是从代码库中抓取一些随机密码实例。让我们首先在Key类型上定义一个成员谓词,检查密钥的大小与给定密码的密钥大小。
正如我们注意到的,这个谓词不限制密码以确保密钥与密码一起使用。让我们向Key添加另一个谓词,可用于获取密钥与之一起使用的所有密码。这意味着密码在对EVP_EncryptInit_ex的调用中作为参数传递,其中使用了密钥。(注意,密钥可能在代码库中的不同位置与不同的密码一起使用。)
就这样!最终的查询,以及一个演示Key和EVP_CIPHER类型如何工作的小测试用例,可以在GitHub上找到。
我的引擎要散架了!
OpenSSL 1.1.1支持在运行时动态加载称为引擎的密码模块。这可用于加载库未实现的自定义算法或与硬件接口。然而,要能够使用引擎,必须首先初始化它,这需要用户按特定顺序调用一些不同的函数。首先,必须选择要加载的引擎,调用引擎初始化函数,然后设置引擎的操作模式。未能初始化引擎可能导致无效输出或分段错误。未能将引擎设置为默认可能意味着OpenSSL使用不同的实现。为了创建一个查询来检测加载的引擎是否被正确初始化,我们决定使用数据流来检查是否调用了正确的函数来初始化加载的引擎。
阅读OpenSSL引擎API的文档后,似乎API用户可以通过几种不同的方式创建引擎对象。我们决定编写一个CodeQL类,同时捕获用户可用于加载新引擎的四个不同函数。(这些函数要么创建新的未选择实例,要么按ID创建新实例,要么使用“previous”和“next”样式函数名从列表中选择引擎。)
接下来,我们需要检查用户是否使用ENGINE_init初始化了新创建的引擎对象,该函数将引擎对象作为参数。此函数不仅初始化引擎,还执行错误检查以确保引擎正常工作。因此,用户不要忘记调用此函数很重要。
用户需要调用的第三个也是最后一个函数是ENGINE_set_default,用于将引擎注册为指定算法的默认实现。Engine_set_default接受一个引擎和一个标志参数。我们创建一个CodeQL类型来表示上面的函数ENGINE_init。
现在我们已经定义了用于使用CodeQL初始化新引擎的函数,我们需要定义相应的数据流应该是什么样子。我们希望确保数据从CreateEngine流向ENGINE_init和ENGINE_set_default。
为了完成此查询并将其整合在一起,如果加载的引擎未传递给ENGINE_init或ENGINE_set_default,我们会标记。完整的查询和相应的测试用例可以在GitHub上找到。
展望未来
OpenSSL libcrypto API充满了可能给开发者带来问题的尖锐边缘。与每个密码实现一样,最小的错误可能导致严重的漏洞。诸如CodeQL之类的工具通过允许开发者和代码审查者构建和共享查询来保护他们的代码,有助于揭示这些问题。我邀请您不仅尝试我们在GitHub存储库中找到的查询(其中还包含Go和C++的额外查询),而且打开您选择的IDE并创建一些您自己的惊人查询!
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容 哦不,别动我的密钥! 我的引擎要散架了! 展望未来 最近的帖子 Trail of Bits的Buttercup在AIxCC挑战赛中获得第二名 Buttercup现已开源! AIxCC决赛:记录表 攻击者的提示注入工程:利用GitHub Copilot 在NVIDIA Triton中发现内存损坏(作为新员工) © 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。