使用CodeQL捕捉OpenSSL误用以提升代码安全
作者:Damien Santiago
日期:2023年12月22日
标签:codeql, internship-projects, cryptography
我创建了五个CodeQL查询,用于捕捉OpenSSL libcrypto API中潜在的严重错误。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
页面内容 最近的帖子 使用Deptective调查您的依赖项 系好安全带,Buttercup,AIxCC的评分回合正在进行中! 使您的智能合约超越私钥风险 Go解析器中意外的安全陷阱 我们从审查Silence Laboratories的首批DKLs23库中学到了什么 © 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。