使用CodeQL捕捉OpenSSL误用
我创建了五个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