检测不良OpenSSL使用模式:Anselm工具的技术解析

本文介绍了Anselm工具如何通过LLVM中间表示检测OpenSSL API的错误使用模式,包括函数调用顺序错误、上下文未初始化及IV重复等问题,提升加密代码的安全性。

检测不良OpenSSL使用模式

OpenSSL是最流行的加密库之一;即使您不使用C/C++,您编程语言的主要库很可能也使用OpenSSL绑定。由于其低级API的设计,OpenSSL也非常容易出错。然而,许多这些错误属于易于识别的模式,这提高了自动化检测的可能性。

作为过去冬季和春季实习的一部分,我一直在开发一个名为Anselm的工具原型,该工具允许开发人员描述和搜索不良行为模式。Anselm是一个LLVM pass,意味着它在源代码和编译之间的中间表示上操作。Anselm相对于静态分析的主要优势是,它可以操作任何编译为LLVM位码的编程语言,或任何可以反向转换的闭源机器代码。Anselm可以针对任意函数调用序列,但其最初目的是检查OpenSSL使用,因此让我们从这里开始。

OpenSSL

OpenSSL的设计使得初学者难以理解和使用。其库中有各种不一致的命名约定,并为每个原语提供了几个( arguably too many)选项和模式。例如,由于库的演变,存在高级(EVP)和低级方法,可用于完成相同的任务(例如DSA签名或EC签名操作)。更糟糕的是,它们的文档可能不一致且难以阅读。

除了难以使用之外,其他设计选择使该库使用危险。API不一致地返回错误代码、指针(有和没有所有权),并展示其他令人惊讶的行为。如果没有严格检查错误代码或防御空指针,可能会出现意外的程序行为和进程终止。

那么Anselm可以检测哪些类型的错误?这取决于开发人员的指定,但可能包括从错误管理OpenSSL错误队列到重用初始化向量的任何内容。记住这些是启发式方法,并且可能误识别良好和不良行为。现在,让我们深入了解工具的工作原理。

函数调用

虽然该项目的主要动机是针对OpenSSL,但库本身实际上并不重要。可以将OpenSSL使用视为一系列API调用,例如EVP_EncryptUpdate和EVP_EncryptFinal_ex。但我们可以轻松地用任何其他名称替换这些名称,想法保持不变。因此,不良行为是任何函数调用(不仅仅是OpenSSL的)的模式,我们希望检测到。

我的主要方法是通过函数中的所有可能执行路径进行搜索,寻找不良的API调用序列。在本文中,我将使用OpenSSL的对称加密函数作为示例。让我们考虑EVP_EncryptUpdate,它加密数据块,和EVP_EncryptFinal_ex,它在最终加密之前填充明文。自然,它们不应被乱序调用:

1
2
3
EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
...
EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);

这也应被标记,因为不良序列仍然可能:

1
2
3
4
5
EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
...
if (condition) {
  EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);
}

我使用LLVM BasicBlocks,它们代表始终一起执行的指令列表。BasicBlocks可以有多个后继,每个反映不同的执行路径。因此,函数是许多BasicBlocks的有向图。有一个根节点,任何叶节点代表执行结束。

找到所有可能的执行相当于从根节点开始执行深度优先搜索(DFS)。然而,注意图可以包含循环;这类似于代码中的循环。如果我们执行盲DFS,可能会陷入无限循环。另一方面,忽略先前访问的节点可能导致错过行为。我通过限制任何路径的长度来解决这个问题,之后Anselm停止进一步探索(这可以自定义)。

还有一个问题,即在整个代码库上执行DFS可能非常耗时。即使我们的确切模式简单,它仍然需要与搜索生成的所有可能路径匹配。为了解决这个问题,我首先修剪图中不包含任何相关API调用的任何BasicBlock。这是通过将任何不相关节点的前驱指向其每个后继来完成的,移除中间人。

在实践中,这显著降低了图的复杂性,以便更快地路径查找:整个if语句和while循环可以被消除而没有任何后果!这也使任何路径限制更加合理。

匹配值

虽然仅检查函数调用是一个好的开始,但我们可以做得更好。考虑OpenSSL上下文,它们由EVP_CIPHER_CTX_new创建,并且必须在实际使用之前用算法、密钥等初始化。在以下情况下,我们希望每个上下文都由EVP_EncryptInit_ex初始化:

1
2
3
EVP_CIPHER_CTX *ctx1 = EVP_CIPHER_CTX_new();
EVP_CIPHER_CTX *ctx2 = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx1, EVP_aes_256_cbc(), NULL, key, iv);

EVP_EncryptInit_ex始终跟随EVP_CIPHER_CTX_new,但ctx2显然未正确初始化。更精确的模式是:“从EVP_CIPHER_CTX_new返回的每个上下文应稍后在EVP_CIPHER_CTX_new中初始化。”

我通过匹配参数和返回值来解决这个问题——检查它们是否指向内存中相同的LLVM Value对象。上下文是匹配值的主要情况,但我们可以使用相同的技术来检测重复的IV:

1
2
EVP_EncryptInit_ex(ctx1, EVP_aes_256_cbc(), NULL, key1, iv);
EVP_EncryptInit_ex(ctx2, EVP_aes_256_cbc(), NULL, key2, iv);

在内部,Anselm使用正则表达式捕获组来执行此分析;每个执行路径是函数调用和Value指针的字符串,而不良行为由某个正则表达式模式定义。

模式格式

在实习结束时,我还定义了一种格式,供开发人员指定不良行为,Anselm将其转换为(有些混乱的)正则表达式模式。每行以函数调用开始,后跟其返回值和参数。如果您不关心值,请使用下划线。否则,定义一个可以在其他地方使用的令牌。因此,禁止重复IV的规则将如下所示:

1
2
EVP_EncryptInit_ex _ _ _ _ _ iv
EVP_EncryptInit_ex _ _ _ _ _ iv

由于iv令牌被重用,Anselm将其搜索限制为仅匹配在该参数位置包含相同Value指针的函数。

我还定义了一种语法来执行负向前瞻,这告诉Anselm寻找特定函数调用的缺失。例如,如果我想防止任何上下文在初始化之前被使用,我会像这样添加感叹号:

1
2
3
EVP_CIPHER_CTX_new ctx
! EVP_EncryptInit_ex _ ctx _ _ _ _
EVP_EncryptUpdate _ ctx _ _ _ _

用英语来说,此模式识别任何对EVP_CIPHER_CTX_new和EVP_EncryptUpdate的调用,这些调用之间没有EVP_EncryptInit_ex夹在中间。

最后说明

凭借其当前的工具集,Anselm能够解释广泛的函数调用模式并在LLVM位码中搜索它们。当然,它仍然是一个原型,还有改进的空间,但主要思想已经存在,我为项目的成果感到自豪。感谢Trail of Bits支持这些类型的实习——非常有趣!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计